[C,C++] HDF入門記録，読み書きの基本操作

# [C,C++] HDF入門記録，読み書きの基本操作

## 環境

- MacOS, Ubuntu
- gcc-9.x
- hdf5 1.12.0

## 目的

[wikipedia](https://ja.wikipedia.org/wiki/Hierarchical_Data_Format) によると，HDFは，

> **Hierarchical Data Format**（階層的データ形式、略称：**HDF**）は、大量のデータを格納および構造化するために設計された一連のファイル形式（**HDF4**、**HDF5**）。

です．

一つのファイルの中に，グループ(ディレクトリのようなもの)とデータセット(ファイルのようなもの)を再帰的に作成することができ，linkや部分圧縮などの高度な機能も持ちます．

また，HDFはマルチプラットフォームであるとともに，多くの言語でサポートされており，例えばPythonだとライブラリh5pyが開発されています．

このHDF5をC,C++から利用したかったのですが，いざ公式のドキュメントを見ると覚えることが多く，初心者には利用までのハードルが高いと感じました．

そこでひとまず，基本機能であるファイルオープン・クローズ，グループの作成，データセットの作成，データセットの(部分)更新機能まで，自分なりに勉強した記録をまとめておくことにします．．

### やること

C,C++からHDFを扱う方法を練習し，手軽に使えるようにまとめます．

公式のexampleは[こちら](https://support.hdfgroup.org/HDF5/examples/intro.html#c)．

といっても，C++はmpi対応していないなどの問題があるらしいので，Cのapiを勉強してラップしていくことにします．



## 取扱い

### 利用を始める

プログラムの中で以下のヘッダーをincludeします．

~~~c++
#include <hdf5.h>
~~~

コンパイル時にはlibraryのリンクを行います

~~~bash
g++ main.cpp -L$(YOUR_HDF_LIBRARY_PATH) -lhdf
~~~

中身を確認するときは，

~~~
h5dump data.h5
~~~

です．HDFViewというGUIも提供されています．



### ファイルのオープン，クローズ

H5Fcreateでファイルが作成できます．H5F...とつくものはファイルハンドルに関連するものです．

~~~c++
char* fname;
hid_t fid = H5Fcreate(fname,H5F_ACC_TRUNC,H5P_DEFAULT,H5P_DEFAULT);
~~~

ここで，H5F_ACC_TRUNCを与えています．これは，open modeで，

|  H5F_ACC_TRUNC   | ファイルが存在しても強制的に上書き   |
| :--------------: | ------------------------------------ |
| **H5F_ACC_EXCL** | **ファイルが存在する場合**，**失敗** |

となっています．H5P_DEFAULTのところを変えるとPropertyを変更できるようですが今回はSkip．

開くときはH5Fopenを使います．

~~~C++
char* fname;
hid_t fid = H5Fopen(fname,H5F_ACC_RDONLY,H5P_DEFAULT);      
~~~

こちらのmodeは

|  H5F_ACC_RDONLY  | 読み込みonly     |
| :--------------: | ---------------- |
| **H5F_ACC_RDWR** | **読み書き可能** |

です．

開いたファイルは，H5Fcloseで閉じるのを忘れないようにしないといけません．

~~~c++
H5Fclose(fid);    
~~~



### グループの作成

H5Gcreateでグループを作成できます．GはGroupのGです．

~~~
char* gname;
H5Gcreate(fid,gname,H5P_DEFAULT,H5P_DEFAULT,H5P_DEFAULT);
~~~



### データセットの作成

データセットを作成するためには，まずH5Screateでdata**S**paceを作成する必要があります．

~~~c++
hsize_t rank = N;
hsize_t dims[N] = {...};
hid_t dataspace = H5Screate_simple(rank,dims,NULL)
~~~

ここで，rankとdimsはそれぞれ$N$次元配列の$N$と，それぞれの要素数($Nx,Ny,Nz$,...)に対応しています．

その後H5Dcreateで**D**atasetを作成します．

~~~c++
char* dname;
hid_t dataset = H5Dcreate(fid,dname,H5T_STD_I32LE,dataspace,H5P_DEFAULT,H5P_DEFAULT,H5P_DEFAULT);
~~~

ここでは4byte(32bit)のint **T**ypeを保存するためのdatasetを作成するためにH5T_STD_I32LEを与えています．

他には例えば，以下があります．

|    Int     | H5T_STD_I32LE      |
| :--------: | ------------------ |
|  **char**  | **H5T_STD_I8LE**   |
| **float**  | **H5T_IEEE_F32LE** |
| **double** | **H5T_IEEE_F64LE** |

作成したデータセットにデータを書き込みます．

~~~c++
int* data; // = [Nx*Ny*Nz*...]
H5Dwrite(dataset,H5T_NATIVE_INT,H5S_ALL,H5S_ALL,H5P_DEFAULT,data);
~~~

H5T_NATIVE_INT, (H5T_NATIVE_CHAR, H5T_NATIVE_FLOAT, H5T_NATIVE_DOUBLE,...)は書き込みデータのtype指定．H5S_ALLはdataspace全てを使うという意味です．部分的な変更については後で書きます．最後の引数にdataの先頭のポインタを渡します．

dataset，dataspaceも使ったあとは忘れずにcloseします．

~~~c++
H5Sclose(dataspace);
H5Dclose(dataset);
~~~



#### データセットのpadding

データセットを何か特定の値で埋めることもできます．これにはH5P(Property)を用います．

~~~c++
int fillval = -1;
hid_t plist = H5Pcreate(H5P_DATASET_CREATE);
H5Pset_fill_value(plist,H5T_NATIVE_INT,&fillval);
hid_t dataset = H5Dcreate(fid,dname,H5T_STD_I32LE,dataspace,H5P_DEFAULT,plist,H5P_DEFAULT);
H5Pclose(plist);
~~~

dataset_createに用いるproperty plistを定義し，fill_valueを与え，H5Dcreateの引数に用います．使用したあとはcloseします．



#### データセットの(部分)更新

あらかじめ確保されたdatasetの(部分)更新を行う場合，流れとしては，datasetのopen，書き込み用の領域をoffsetなど指定して書き込むという感じになります．他にもstrideやblockも指定できるようですが一旦割愛します，

~~~c++
int* data;
char* dname;
hid_t dataset = H5Dopen(fid,dname,H5P_DEFAULT);
hid_t dataspace = H5Dget_space(dataset);
hsize_t rank;
hsize_t dims[rank];
hsize_t offsets[rank];
hid_t dataspace_buf = H5Screate_simple(rank,dims,NULL);
H5Sselect_hyperslab(dataspace,H5S_SELECT_SET,offsets,NULL,dims,NULL);
H5Dwrite(dataset,H5T_NATIVE_INT,dataspace_buf,dataspace,H5P_DEFAULT,data);
~~~



### データセットの読み込み

作成したdatasetをopenし，dataspaceを取得します．とりあえずこれをreadに渡すとdataset全体を読み出せます．

~~~c++
hid_t dataset = H5Dopen(fid,dname,H5P_DEFAULT);
hid_t dataspace = H5Dget_space(dataset);
H5Dread(dataset,H5T_NATIVE_INT, dataspace, dataspace, H5P_DEFAULT, data);
~~~

以上で，

- ファイルのopen close
- グループの作成
- データセットを配列データから作成，または，ある値で初期化する．
- データセットを更新する
- データセットを読み込む

という操作ができるようになりました．本当はparallel hdfまで勉強したかったのですがまた今度...

### 機能をまとめてみる

個人的にはh5pyの使用感が好きなので，少し意識しつつ，以上の機能を構造体にまとめてみました．

~~~c++
#include<hdf5.h>
#include<cassert>
#include<iostream>
#include<vector>
#include<string>
#include<type_traits>

template<typename T>
constexpr bool false_v = false;

template <typename T>
hid_t get_predtype()
{
  if constexpr (std::is_same<T,char>{}){
    return H5T_NATIVE_CHAR;
  }else if constexpr (std::is_same<T,int>{}){
    return H5T_NATIVE_INT;  
  }else if constexpr (std::is_same<T,float>{}){
    return H5T_NATIVE_FLOAT;  
  }else if constexpr (std::is_same<T,double>{}){
    return H5T_NATIVE_DOUBLE;  
  }else{
    static_assert(false_v<T>,"cannot find predtype");
  }
  return 0;
}

template <typename T>
hid_t get_dspace_type()
{
  if constexpr (std::is_same<T,char>{}){
    return H5T_STD_I8LE;
  }else if constexpr (std::is_same<T,int>{}){
    return H5T_STD_I32LE;  
  }else if constexpr (std::is_same<T,float>{}){
    return H5T_IEEE_F32LE;  
  }else if constexpr (std::is_same<T,double>{}){
    return H5T_IEEE_F64LE;  
  }else{
    static_assert(false_v<T>,"cannot find dspacetype");
  }
  return 0;
}



struct h5fp
{
private:
  hid_t fid;

public:
  h5fp(std::string fname, char mode)
  { open(fname,mode); };

  ~h5fp()
  { close(); }

  void open(std::string fname, char mode)
  {
    switch (mode)
    {
    case 'w':
      fid = H5Fcreate(fname.c_str(),H5F_ACC_TRUNC,H5P_DEFAULT,H5P_DEFAULT);
      break;
    case 'r':
      fid = H5Fopen(fname.c_str(),H5F_ACC_RDONLY,H5P_DEFAULT);      
      break;    
    default:
      assert(!"[error] invalid mode from h5f::open"); 
      break;
    }
  }

  void close()
  {
    H5Fclose(fid);    
    return;
  }

  void create_group(std::string gname)
  {
    hid_t gid = H5Gcreate(fid,gname.c_str(),H5P_DEFAULT,H5P_DEFAULT,H5P_DEFAULT);
    H5Gclose(gid);
    return;
  }

  template<typename T>
  void create_dataset(std::string dname,std::vector<int> shape,T data[])
  {
    size_t sz = shape.size();
    hsize_t rank = (hsize_t)sz;
    hsize_t dims[sz];
    for (size_t i = 0; i < sz; i++) dims[i] = (hsize_t)shape[i];
    hid_t dataspace = H5Screate_simple(rank,dims,NULL);
    hid_t dataset = H5Dcreate(fid,dname.c_str(),get_dspace_type<T>(),dataspace,H5P_DEFAULT,H5P_DEFAULT,H5P_DEFAULT);
    H5Dwrite(dataset,get_predtype<T>(),H5S_ALL,H5S_ALL,H5P_DEFAULT,data);
    H5Dclose(dataset);
    H5Sclose(dataspace);
    return;
  }

  template<typename T>
  void update(std::string dname,std::vector<int> shape,std::vector<int> offset,T data[])
  {
    hid_t dataset = H5Dopen(fid,dname.c_str(),H5P_DEFAULT);
    hid_t dataspace = H5Dget_space(dataset);
    size_t sz = shape.size();
    hsize_t rank = (hsize_t)sz;
    hsize_t dims[sz];
    hsize_t offsets[sz];
    for (size_t i = 0; i < sz; i++){ 
      dims[i] = (hsize_t)shape[i];
      offsets[i] = (hsize_t)offset[i];
    }
    hid_t dataspace_buf = H5Screate_simple(rank,dims,NULL);
    H5Sselect_hyperslab(dataspace,H5S_SELECT_SET,offsets,NULL,dims,NULL);
    H5Dwrite(dataset,get_predtype<T>(),dataspace_buf,dataspace,H5P_DEFAULT,data);
    H5Dclose(dataset);
    H5Sclose(dataspace);
    return;
  }

  template<typename T>
  void create_dataset(std::string dname,std::vector<int> shape,T fillval)
  {
    size_t sz = shape.size();
    hsize_t rank = (hsize_t)sz;
    hsize_t dims[sz];
    for (size_t i = 0; i < sz; i++) dims[i] = (hsize_t)shape[i];
    hid_t dataspace = H5Screate_simple(rank,dims,NULL);
    hid_t plist = H5Pcreate(H5P_DATASET_CREATE);
    H5Pset_fill_value(plist,get_predtype<T>(),&fillval);
    hid_t dataset = H5Dcreate(fid,dname.c_str(),get_dspace_type<T>(),dataspace,H5P_DEFAULT,plist,H5P_DEFAULT);
    H5Dclose(dataset);
    H5Sclose(dataspace);
    H5Pclose(plist);
    return;
  }

  template<typename T>
  void read_dataset(std::string dname, T data[] )
  {
    hid_t dataset = H5Dopen(fid,dname.c_str(),H5P_DEFAULT);
    hid_t dataspace = H5Dget_space(dataset);
    H5Dread(dataset, get_predtype<T>(), dataspace, dataspace, H5P_DEFAULT, data);
    H5Sclose(dataspace);
    H5Dclose(dataset);
    return;
  }

  std::vector<hsize_t> get_shape(std::string dname)
  {
    hid_t dataset = H5Dopen(fid,dname.c_str(),H5P_DEFAULT);
    hid_t dataspace = H5Dget_space(dataset);
    hsize_t rank = H5Sget_simple_extent_ndims(dataspace);
    std::vector<hsize_t> dims(rank);
    H5Sget_simple_extent_dims(dataspace,dims.data(),NULL);
    H5Dclose(dataset);
    H5Sclose(dataspace);    
    return dims;
  }


};

~~~



使って遊んでみました．

~~~c++
  float a[5][8];
  for (int i = 0; i < 5; i++)
  {
    for (int j = 0; j < 8; j++)
    {
      a[i][j] = (i+1)*(j+1);
    }    
  }
  double c[6]={0};
  for (int i = 0; i < 6; i++)
  {
    c[i] = 10.0;
  }
    
  {
    h5fp fp("data.h5",'w');
    fp.create_group("/g");
    fp.create_group("/g/gg1");
    fp.create_group("/g/gg2");
    std::cout<<"created groups"<<std::endl;
    fp.create_dataset("/g/gg1/a",{5,8},&a[0][0]);
    fp.create_dataset("/g/gg2/c",{10,6},0.0);
    for (int i = 0; i < 8; i++)
    {
      for (int j = 0; j < 6; j++)
      {
        c[j] = i;
      }      
      fp.update("/g/gg2/c",{1,6},{i,0},c);
    }    
    std::cout<<"wrote a"<<std::endl;
  }


  int b[5][8];
  for (int i = 0; i < 5; i++)
  {
    for (int j = 0; j < 8; j++)
    {
      b[i][j] = -100;
    }    
  }

  {
    h5fp fp("data.h5",'r');
    std::cout<<"reopened"<<std::endl;
    fp.read_dataset("/g/gg1/a",&b[0][0]);
    std::cout<<"read /g/gg1/a"<<std::endl;
    for (size_t i = 0; i < 5; i++)
    {
      for (size_t j = 0; j < 8; j++)
      {
        std::cout<<b[i][j]<<" ";
      }
      std::cout<<std::endl;
    }
  }

~~~

~~~
created groups
wrote areopened
read /g/gg1/a1 
2 3 4 5 6 7 8 2 4 6 8 10 12 14 16 3 6 9 12 15 18 21 24 4 8 12 16 20 24 28 32 5 10 15 20 25 30 35 40 
~~~



シンプルですが自分で使う分には悪くないと思います．もっと機能が欲しいですが，その辺は必要に応じて勉強して増やす感じですかね

## 追記: parallel HDFへの対応

### 利用の準備
buildする場合，--enable-parallelが必要です．

~~~bash
./configure --prefix=$(YOUR_INSTALL_PATH) --enable-parallel
~~~

Homebrewの場合，

~~~bash
brew install hdf5-mpi
~~~

です．

### ファイルのオープン

ファイルアクセスハンドラを渡す必要があります．

~~~c++
hid_t plist_id = H5Pcreate(H5P_FILE_ACCESS);
                 H5Pset_fapl_mpio(plist_id,MPI_COMM_WORLD,MPI_INFO_NULL);
fid = H5Fcreate(fname.c_str(),H5F_ACC_TRUNC,H5P_DEFAULT,plist_id);
H5Pclose(plist_id);
~~~

後は普通のhdfと同じように扱います．offsetなどをmpi_rankによって変えながら同一のデータセットに書き出せば良いだけです．

上記でまとめた構造体を使うなら，以下のような変更を加えます．

~~~c++
  void open(std::string fname, char mode)
  {
    hid_t plist_id = H5Pcreate(H5P_FILE_ACCESS);
                     H5Pset_fapl_mpio(plist_id,MPI_COMM_WORLD,MPI_INFO_NULL);

    switch (mode)
    {
    case 'w':
      fid = H5Fcreate(fname.c_str(),H5F_ACC_TRUNC,H5P_DEFAULT,plist_id);
      break;
    case 'r':
      fid = H5Fopen(fname.c_str(),H5F_ACC_RDONLY,plist_id);      
      break;    
    default:
      assert(!"[error] invalid mode from h5f::open"); 
      break;
    }
    H5Pclose(plist_id);
    return;
  }

  void close()
  {
    int already_finalized;
    MPI_Finalized(&already_finalized);
    if( !already_finalized ){
      H5Fclose(fid);    
    }else{
      H5Fclose(fid);    
      assert(!"coding error! you must destroy ph5fp before MPI_FINALIZE");
    }
    return;
  }
~~~

以下はお試しで作った拡散方程式を計算するコードの一部です．
呼び出す際，以下のようにoffsetをrankに依存させれば，独立の場所に書き出せます．

~~~c++
fp.create_dataset(dname,{p.Nx,p.Ny},0.0);
for (int i = 1; i < p.Nx_local+1; i++){
  fp.update(dname,{1,p.Ny},{p.Nx_local*p.mpi_rank+(i-1),0},&v[i][1]);
}
~~~

## 後書き

1年くらい前にc++ apiを一瞬使っていたのですが記憶が吹っ飛んでいました．日頃，まとめるのが大事なんだとは思っているのですが実行するのが...