読者です 読者をやめる 読者になる 読者になる

cockscomblog?

cockscomb on hatena blog

Fortran で NetCDF データを読み込む

エンジニアであれば、Fortran で NetCDF データを読み込むなんてこともよくありますよね。だから今日はそのやり方を簡単に説明します!

前提

Fortran っていうのは、数値計算とかに使う計算が速い言語のことです。NetCDF は大気海洋系の人たちがよく使う、多次元配列データを複数まとめてメタデータといっしょに記録できるバイナリ形式の共通フォーマットです。

OS X ユーザーだったら、とりあえず Homebrew を使って

brew install gfortran
brew install netcdf --enable-fotran

とかしておきましょう。ポイントとしては、NetCDF をインストールしてから gfortran のバージョンが変わるとインクルードに失敗することがあるので、そういうときは面倒だから NetCDF 入れ直すといいです。

今回は例として NOAA の “NCEP/NCAR Reanalysis 1: Surface Flux” から、Daily の “Downward solar radiation flux” を使いましょう。適当に2006年のデータをダウンロードしておきました。

ncdump

NetCDF をインストールすると ncdump コマンドが使えるようになります。ダウンロードした拡張子が .nc のファイルのあるところにワーキングディレクトリを移動して、試しに

ncdump -c dswrf.sfc.gauss.2006.nc

としてみましょう。ごちゃっといろいろ表示されると思います。ncdump は NetCDF ファイルをダンプするコマンドで、-c オプションを使うことでヘッダ情報と次元情報を見ることができます。オプションなしだとデータも出ます。-h だとヘッダだけ見られます。

netcdf dswrf.sfc.gauss.2006 {
dimensions:
    lon = 192 ;
    lat = 94 ;
    time = UNLIMITED ; // (365 currently)
variables:
    float lat(lat) ;
        lat:units = "degrees_north" ;
        lat:actual_range = 88.542f, -88.542f ;
        lat:long_name = "Latitude" ;
        lat:standard_name = "latitude" ;
        lat:axis = "Y" ;
    float lon(lon) ;
        lon:units = "degrees_east" ;
        lon:long_name = "Longitude" ;
        lon:actual_range = 0.f, 358.125f ;
        lon:standard_name = "longitude" ;
        lon:axis = "X" ;
    double time(time) ;
        time:units = "hours since 1-1-1 00:00:0.0" ;
        time:long_name = "Time" ;
        time:actual_range = 17575512., 17584248. ;
        time:delta_t = "0000-00-01 00:00:00" ;
        time:avg_period = "0000-00-01 00:00:00" ;
        time:standard_name = "time" ;
        time:axis = "T" ;
    short dswrf(time, lat, lon) ;
        dswrf:long_name = "mean Daily Downward Solar Radiation Flux at surface" ;
        dswrf:unpacked_valid_range = -120.f, 1300.f ;
        dswrf:actual_range = -3.5f, 568.f ;
        dswrf:units = "W/m^2" ;
        dswrf:add_offset = 3156.5f ;
        dswrf:scale_factor = 0.1f ;
        dswrf:missing_value = 32766s ;
        dswrf:precision = 1s ;
        dswrf:least_significant_digit = 0s ;
        dswrf:GRIB_id = 204s ;
        dswrf:GRIB_name = "DSWRF" ;
        dswrf:var_desc = "Downward Solar Radiation Flux" ;
        dswrf:dataset = "NCEP Reanalysis Daily Averages" ;
        dswrf:level_desc = "Surface" ;
        dswrf:statistic = "Mean" ;
        dswrf:parent_stat = "Individual Obs" ;
        dswrf:valid_range = -32765s, -18565s ;

// global attributes:
        :Conventions = "COARDS" ;
        :title = "mean daily NMC reanalysis (2006)" ;
        :history = "created 2006/01/03 by Hoop (netCDF2.3)" ;
        :description = "Data is from NMC initialized reanalysis\n",
            "(4x/day).  It consists of T62 variables interpolated to\n",
            "pressure surfaces from model (sigma) surfaces." ;
        :platform = "Model" ;
        :references = "http://www.esrl.noaa.gov/psd/data/gridded/data.ncep.reanalysis.html" ;
}

-h したのをそのまま貼り付けました。

netcdf ファイル名 {
dimensions:
    次元名 = 次元の長さ ;
    ...
variables:
    型 変数名(次元名) ;
        変数名:属性名 = 属性値;
        ...
    ...
// global attributes:
    :グローバル属性名 = グローバル属性値 ;
    ...
}

こういう感じで表されています。わかりやすいですね。次元の中に長さが UNLIMITED となっているものがありますが、これは無制限に続けることができるということです。次元と変数で同じ名前のものがありますが、これはある次元で何番目だったら実際にはこの値というのが分かるようになっています。変数は次元を持ち、その個数に応じてN次元の配列になります。変数毎の属性はけっこう大事で、単位 units はもちろん、いろいろ重要な情報が記録されているので、必ず一通り確認しましょう。

Fortran

実際に Fortran で読み込んでみます。これには、NetCDF の Fortran 90 インターフェースを使います。基本的にはドキュメントにまとまっているので、必要に応じて確認しましょう。

先に実際のプログラムを載せておきます。

program netcdf_read
    use netcdf
    implicit none

    ! NetCDF file
    character (len=*), parameter :: FILE_NAME = "dswrf.sfc.gauss.2006.nc"
    integer :: ncid

    ! Dimensions
    integer, parameter :: NDIMS = 3
    integer :: lon_dim, lat_dim, time_dim
    real, allocatable :: lats(:), lons(:), times(:)

    ! Variable
    character(len=*), parameter :: VAR_NAME="dswrf"
    integer :: var_varid
    real, allocatable :: vars(:, :)
    real :: add_offset, scale_factor

    integer :: start(NDIMS), count(NDIMS)

    integer :: lat, lon, time
    integer :: err

    ! Open NetCDF file
    call check( nf90_open(FILE_NAME, nf90_nowrite, ncid) )

    ! Get dimension data
    call get_dimension(ncid, 'lon', lon_dim, lons)
    call get_dimension(ncid, 'lat', lat_dim, lats)
    call get_dimension(ncid, 'time', time_dim, times)

    ! Allocate variable
    allocate(vars(lat_dim, lon_dim), stat=err)
    if (err /= 0) print *, "Allocation request denied"
    ! Get variable id
    call check( nf90_inq_varid(ncid, VAR_NAME, var_varid) )
    ! Get add_offset and scale_factor
    call check( nf90_get_att(ncid, var_varid, 'add_offset', add_offset) )
    call check( nf90_get_att(ncid, var_varid, 'scale_factor', scale_factor) )

    ! Initialize count and start for reading data
    count = (/ lon_dim, lat_dim, 1 /)
    start = (/ 1, 1, 1 /)

    do time = 1, time_dim
        start(3) = time
        ! Read data
        call check( nf90_get_var(ncid, var_varid, vars, start = start, &
            count = count) )
        ! Restore values to original ones
        vars = vars * scale_factor + add_offset
        
        do lat = 1, lat_dim
            do lon = 1, lon_dim
                ! You can do anything here
                ! write(*, *) times(time), lats(lat), lons(lon), vars(lon, lat)
            end do
        end do
    end do

    ! Close NetCDF file
    call check( nf90_close(ncid) )

    ! If we got this far, everything worked as expected. Yipee! 
    print *, "*** SUCCESS reading example file ", FILE_NAME, "!"

contains
    subroutine get_dimension(ncid, name, dim, dims)
        integer,           intent( in) :: ncid
        character(len=*),  intent( in) :: name
        integer,           intent(out) :: dim
        real, allocatable, intent(out) :: dims(:)

        integer :: varid, dimid

        call check( nf90_inq_dimid(ncid, name, dimid) )
        call check( nf90_inquire_dimension(ncid, dimid, len=dim) )
        allocate(dims(dim), stat=err)
        if (err /= 0) print *, name, ": Allocation request denied"
        call check( nf90_inq_varid(ncid, name, varid) )
        call check( nf90_get_var(ncid, varid, dims) )
    end subroutine get_dimension

    subroutine check(status)
        integer, intent(in) :: status

        if (status /= nf90_noerr) then 
            print *, trim(nf90_strerror(status))
            stop "Stopped"
        end if
    end subroutine check  
end program netcdf_read

これを分割して説明を加えていきます。

program netcdf_read
    use netcdf
    implicit none

use netcdf することで NetCDF ライブラリの関数を使うと宣言しています。これはコンパイル時に必要です。また必ず implicit none してください。これがない Fortran は認めません。

ここからは変数宣言です。

    ! NetCDF file
    character (len=*), parameter :: FILE_NAME = "dswrf.sfc.gauss.2006.nc"
    integer :: ncid

ファイル名を定数にしています。定数を大文字にするのはよい習慣だと思います。ncid というのは、NetCDF ファイルを開いたあとに使うファイルの識別子です。

    ! Dimensions
    integer, parameter :: NDIMS = 3
    integer :: lon_dim, lat_dim, time_dim
    real, allocatable :: lats(:), lons(:), times(:)

次元に関する変数宣言をしています。後で使います。配列を allocatable にしているのは、実際の長さをデータに応じて変えるためです。( times は NetCDF で double になっているので real(8) にすべきですが、特に差がないので `real(4) にしています。 )

    ! Variable
    character(len=*), parameter :: VAR_NAME="dswrf"
    integer :: var_varid
    real, allocatable :: vars(:, :)
    real :: add_offset, scale_factor

NetCDF 内の変数についての変数宣言です。ここでも識別子となる var_varid があります。vars が2次元の配列なのは、実際には3次元だけど一度に読み込むのは2次元分にしたいからです。add_offsetscale_factor は後で説明します。

    integer :: start(NDIMS), count(NDIMS)

    integer :: lat, lon, time
    integer :: err

ファイルを読み込むループに使う変数とかです。

ここから実際の処理に移ります。

    ! Open NetCDF file
    call check( nf90_open(FILE_NAME, nf90_nowrite, ncid) )

nf90_open 関数で NetCDF ファイルを開きます。check サブルーチンは下の方で定義されていて後で説明しますが、エラーハンドリングのために用意しています。nf90_ というのが NetCDF の関数で、返り値として成否を返すので、これを check サブルーチンで確かめています。nf90_nowrite としているのは予期せず書き込んでしまうことを防ぐため、読み込み専用にしたいからです。これでファイルが開かれ、ncid にファイルと対応した識別子が代入されます。

    ! Get dimension data
    call get_dimension(ncid, 'lon', lon_dim, lons)
    call get_dimension(ncid, 'lat', lat_dim, lats)
    call get_dimension(ncid, 'time', time_dim, times)

ここでは次元の情報を後で定義するサブルーチン get_dimension で得ています。同じ処理が続くのでサブルーチンにしてあります。後で説明しますが、これで各次元の名前から長さと実際の値を取り出しています。

    ! Allocate variable
    allocate(vars(lat_dim, lon_dim), stat=err)
    if (err /= 0) print *, "Allocation request denied"

ここで allocatable にしてあった多次元配列 vars を実際に初期化します。次元の長さが分からないと配列の大きさも決まらないので、ここで初期化するのがよいでしょう。

    ! Get variable id
    call check( nf90_inq_varid(ncid, VAR_NAME, var_varid) )

ファイルと同じように、変数にも識別子があります。それをここで nf90_inq_varid を用いて取得します。

    ! Get add_offset and scale_factor
    call check( nf90_get_att(ncid, var_varid, 'add_offset', add_offset) )
    call check( nf90_get_att(ncid, var_varid, 'scale_factor', scale_factor) )

nf90_get_att は属性値を得るための関数です。変数の識別子と属性名を使って値を取り出します。

add_offsetscale_factor は、NetCDF データの容量削減と精度向上のために、予め変数の値を一定のルールで操作されているときに記録されている属性です。var = (var - add_offset) * 1/scale_vactor されているので、逆に var = var * scale_factor + add_offset してやる必要があります。そのためにここで値を得ておきます。

    ! Initialize count and start for reading data
    count = (/ lon_dim, lat_dim, 1 /)
    start = (/ 1, 1, 1 /)

ファイルを読み込むとき、一気に読み込むとメモリを多量に使ってしまうことになりがちです。今回は real としているので、仮にすべて読み込んだとしても25 MB程度に収まるはずですが、これより大きなデータを扱うことも多いでしょうから、今回も time 次元について分割して読み込んでいきます。

次に出てくる変数の値を読み込むところで、startcount を使います。変数の次元毎に読み込みの開始と量を設定できます。

    do time = 1, time_dim
        start(3) = time
        ! Read data
        call check( nf90_get_var(ncid, var_varid, vars, start = start, &
            count = count) )
        ! Restore values to original ones
        vars = vars * scale_factor + add_offset

時間次元のループの中で nf90_get_var 関数を使って、ファイル識別子と変数識別子から、vars 変数に値を読み込みます。startcount が使われています。その前に start(3) = time とすることで、読み込み場所をカウンタ変数 time によって設定できています。さらにここで vars = vars * scale_factor + add_offset としていて、配列すべてについて add_offsetscale_factor を考慮しています。

        do lat = 1, lat_dim
            do lon = 1, lon_dim
                ! You can do anything here
                ! write(*, *) times(time), lats(lat), lons(lon), vars(lon, lat)

いよいよここで実際の値を処理することができます。

            end do
        end do
    end do

    ! Close NetCDF file
    call check( nf90_close(ncid) )

    ! If we got this far, everything worked as expected. Yipee! 
    print *, "*** SUCCESS reading example file ", FILE_NAME, "!"

最後に nf90_close でファイルを閉じて、処理が終了したことを出力します。

この下は、このプログラム中で使うサブルーチンです。

contains
    subroutine get_dimension(ncid, name, dim, dims)
        integer,           intent( in) :: ncid
        character(len=*),  intent( in) :: name
        integer,           intent(out) :: dim
        real, allocatable, intent(out) :: dims(:)

        integer :: varid, dimid

get_dimension サブルーチンでは、次元名を受け取って次元の長さと各値を返します。nf90_inq_dimid で次元名から次元の識別子を取り出し、nf90_inquire_dimension で次元の長さを得ます。あとはそれを使って allocate して、変数を取り出すだけです。

        call check( nf90_inq_dimid(ncid, name, dimid) )
        call check( nf90_inquire_dimension(ncid, dimid, len=dim) )
        allocate(dims(dim), stat=err)
        if (err /= 0) print *, name, ": Allocation request denied"
        call check( nf90_inq_varid(ncid, name, varid) )
        call check( nf90_get_var(ncid, varid, dims) )
    end subroutine get_dimension

    subroutine check(status)
        integer, intent(in) :: status

check サブルーチンは、nf90_ で始まる関数が返すステータスが nf90_noerr かどうかを確認し、エラーであったときには nf90_strerror 関数でエラーに対応する文字列を得て出力し、プログラムを終了します。

        if (status /= nf90_noerr) then 
            print *, trim(nf90_strerror(status))
            stop "Stopped"
        end if
    end subroutine check  
end program netcdf_read

ここまでがプログラムの説明です。わかりやすいですね。

コンパイル

Homebrew でデフォルトの場所にインストールされた NetCDF を使うには

gfortran ファイル名.f90 -I/usr/local/include -L/usr/local/lib -lnetcdff -o ファイル名

みたいな感じにします。インクルードする netcdf.mod がある場所を -I オプションで、リンクする libnetcdff.a がある場所を -L オプションで指定し、-lnetcdff でリンクします。それぞれの場所は環境に合わせて変えてください。

まとめ

これで Fortran を使って NetCDF ファイルを読み込むことができましたね。今回のケースは変数などの名前が分かっているという前提ですが、そうでないときにもちゃんと読み込むことができます。あと書き込むことももちろんできますが、そういうのは今回は取り扱っていません。ドキュメント読めば何とかなります。

add_offset とか scale_factor とか、気をつけないと全然違う値になります。ちょっと面倒ですね。そういうのも勝手に何とかなっていたらいいですよね。というかそもそもいろいろ面倒ですね。

そこでオススメするのが Python の netcdf4 モジュールです。だいたいうまくやってくれるし、データが numpy で取り扱えるので、そのまま matplotlib でプロットもできます。記述量も減って簡単になるし、良いことしかないですね。ということで、Fortran で読み込むのやめて Python で読み込むといいですよ。