のらりくらり

物理化学分野のポスドクです。プログラミング、読書、自転車などが好きです。

HDF5フォーマットに関するメモ書き その2

前に「HDF5フォーマットに関するメモ書き」なるものを書いてからずいぶん間が開いてしまいましたが「その2」を書こうと思います。
どれだけやる気無かったんだよと言われそうだけど、きっとそんなに緊急の需要は無いだろうから良しということで。
もし誰かが急にプロジェクトの方向性でHDF5を使わざるを得ない状況になった時にとりあえず初歩を知るためのリソースとして検索でたどり着いてもらえれば嬉しいです。

さて今回はC++を用いてHdf5にデータを記録する方法について説明します。僕が主に使っていたのは2次元の配列(matrix)とか、構造体の1次元配列くらいで、今回は後者に絞って説明します。構造体の型を定義してデータを記録して、という手順だけ理解できれば、matrixは配布されているサンプルソースを読めばすぐに理解できるかと思います。
C++で読みこむ方法についてはとりあえずその1で説明したhdf5viewがあれば中身は見ることができるので、今回は書きません。
また、書き込みの一連の流れにおいて、実際のデータを渡すのはある程度プログラミングの経験があればすぐ理解できるものだと思いますので、その流れの中での手続きに関するお話が中心です。

とりあえずコードと結果を比べてみる

一つシンプルなコードを書いてみたので、今回はそれを例に考えてみましょう。これは人の年齢、性別、名前、身長を記録する構造体を3人分にわたって記録するコードです。
分かる方はここだけ見れば後は差し当たり不自由無い程度に使えるようになるのでは無いかと思います。

これをmakeして実行すると、
f:id:salondunord:20130823224400p:plain

のようなデータがデータが記録されるはずです。
なお、このコードがmakefileとセットで一式必要な場合は、
https://gist.github.com/6319458.git
からgitクローンしてください。

データ型のお話

上のコードを見ていただければ分かると思いますが、Hdf5のデータの記録は、プログラム上の流れとして、

  1. 記録するデータの準備
  2. 準備したデータの型をHdf5に渡すための準備
  3. データセットを作成して、データの型をHdf5に教える
  4. 実際に書き込む処理

というようになります。
ここでは、二つ目の「準備したデータの型をHdf5に渡すための準備」の部分について見てみます。
まず、Hdf5はCSVみたいに、テキストデータをある一定のルールで解釈していくという方式ではなく、Hdf5ファイル自体が記録されているデータの型や個数などの情報を保持しています。従って、実際に書き込むためのデータを受け渡すよりも前に、書き込むデータがどんなデータ型、あるいはそれらを組み合わせた構造体であるかを教えてやる必要があります。
上記のコードで出てくる構造体には、

typedef struct {
    int age;
    char sex;
    char name[MAX_NAME_LENGTH];
    float height;
} PersonalInformation;

のように、3つのプリミティブ型(age, sex, height)と、一つの複合データ型(name)が含まれています。コード中の

    // defining the datatype to pass HDF55
    H5::CompType mtype(sizeof(PersonalInformation));
    mtype.insertMember(member_age, HOFFSET(PersonalInformation, age), H5::PredType::NATIVE_INT);
    mtype.insertMember(member_sex, HOFFSET(PersonalInformation, sex), H5::PredType::C_S1);
    mtype.insertMember(member_name, HOFFSET(PersonalInformation, name), H5::StrType(H5::PredType::C_S1, MAX_NAME_LENGTH));
    mtype.insertMember(member_height, HOFFSET(PersonalInformation, height), H5::PredType::NATIVE_FLOAT);

の部分で、この構造体の型をhdf5に教えるための準備をしており、これを1行ずつ見て行きます。
まず、H5::CompTypeは、構造体の型をHdf5に伝えるときに使用されるデータ型です。これをコンストラクトする時に記録する構造体の大きさを渡します。
その後の3行にわたって、insertMemberメンバ関数を用いて、この構造体の中身の情報を登録しています。
第1引数は、要素の名前を表すstring型です。HDFViewで開いたときに表の一番上に項目名としてここで登録した文字列が表示されます。Hdf5公式から提供されているサンプルではH5::H5std_stringという型を使っていますがどっちでも殆ど同じです。
第2引数は、構造体の中でのそれぞれのフィールドのオフセットを渡しています。これにはHOFFSETというマクロが提供されており、これに構造体の型名とフィールド名を渡すと、自動的に計算されます。
第3引数が、このフィールドの保持するデータの型を表しています。
見ていただければ簡単に分かるかと思いますが、intにはH5::PredType::NATIVE_INT、floatにはNATIVE_FLOATというように渡しています。また、charに対してはNATIVE_CHARという型名もあるのですが、やってみたところ、これだと数字が表示されてしまい、意図したように文字を表示させたいときは、H5::PredType::C_S1を使う必要があるみたいです。そして、名前(文字列)は、C言語ではcharの配列と表されますが、これに関しては、hdf5に対して

H5::StrType(中身の型、長さ)

を渡します。ここでは複合データ型の例としては、H5::StrTypeしか使用していませんが、他にも、H5::ArrayTypeという、多次元配列までを表せる複合データ型も存在します。気になる方はinclude/H5ArrayType.hなどに宣言があるので見てみてください。
また、ここで用いているNATIVE_XXXという型はそのシステムで用いられているnativeな型を示しています。例えばMacを使っている場合、intは実際には32bit, little endian, signedの型が暗黙のうちに使われていますので、自動的にこの型の事だと解釈されます。
しかし、実際にEndianの指定や、stdint.hに書いてあるような幅などをきちんと指定した型でデータ型を渡したいときには、

       // Declaration of predefined types; their definition is in H5PredType.cpp
       //〜省略〜
        static const PredType STD_I16BE;
        static const PredType STD_I16LE;
        static const PredType STD_I32BE;
        static const PredType STD_I32LE;
        static const PredType STD_I64BE;
        static const PredType STD_I64LE;
        static const PredType STD_U8BE;
        static const PredType STD_U8LE;
        static const PredType STD_U16BE;
        static const PredType STD_U16LE;
        static const PredType STD_U32BE;
        static const PredType STD_U32LE;
        static const PredType STD_U64BE;
        static const PredType STD_U64LE;
    //〜省略〜

などの型も準備されています。本来はこちらを使う方が安全でしょう。詳しくは、include/H5PredType.hを見てみてください。
このようにして、それぞれの要素の型を登録できたら、実際の書き込みの段階に入って行きます。

書き込みの準備のお話

これに関するコードは上記の中の

    // preparation of a dataset and a file.
    H5::DataSpace space(rank, dim);
    H5::H5File *file = new H5::H5File(FileName, H5F_ACC_TRUNC);
    H5::DataSet *dataset = new H5::DataSet(file->createDataSet(DatasetName, mtype, space));
    // Write
    dataset->write(person_list, mtype);

の部分です。難しくないです。
 
 まず、DataSpaceという型のオブジェクトを作ります。これは記録するデータ全体の長さを表しています。コンストラクト時に渡しているrank, dimの定義は少し上、

    // the array of each length of multidimentional data.
    hsize_t dim[1];
    dim[0] = sizeof(person_list) / sizeof(PersonalInformation);

    // the length of dim
    int rank = sizeof(dim) / sizeof(hsize_t);

で、dimはデータセットの各方向の長さを表しています。といっても今回は構造体の1次元の配列なので、長さは一つで良いのでどうしてこのようにするのか分かりにくいですが、多次元配列を登録するときは、ここに各次元の要素数を配列にして定義します。
そして、このときの次元数を表しているのがその次のrankです。要はdimの配列の長さです。
この2つの値から、登録するオブジェクトがどのくらいの次元で、それぞれの長さはいくらなのか、というのを保持するのがここで作成しているDataSpaceという型でした。

 次にfileを開きます。第二引数はファイルに書き込むときはH5F_ACC_TRUNCにしておけば前からあったデータを消した上で0から書き込むようになります。その他のオプションはヘッダファイルを見てみてください。
 
 そしてDataSetオブジェクトです。ここまでくればコードを見るだけで分かるとおもいますが、fileに対して新しいデータセットを、データセットの名前、記録する構造体の型、要素数を表すspaceオブジェクトを渡す事で作成する事を示しています。ここまでくれば、後は実際の生データを渡すだけです。
それがdataset->write()の部分ですね。

以上でとりあえず任意の構造体を書き込む流れは終了です。もし途中でエラーなどがあれば例外がスローされるはずですがうまく行けば怒られずにプログラムは終了するはずです。適宜HDFViewで開いて確認してみましょう。

最後に

ここまでの流れが分かればサンプルとして配布されているcreate.cppなりを読めばわかると思います。要はmatrixに適したデータ型を指定・登録して実際のデータを渡して書き込むだけです。

気が向けば次はGroupの話を書くかもしれませんが期待は絶対にしないようにしましょう。
途中からチラ裏状態になって(最初からだボケ!という突っ込みは無しで)すいません。終わり。