前書き
HDF(Hierarchical Data Structure)が何かという説明は省略します(ここにいらした方は既にご存じと思いますので)。ちゃんと知りたいという方は、上記本家ウェブページかWikipediaをご参照下さい。
本記事ではHDF5を扱います。既に普及しているデータフォーマットなので関連記事はいろいろ見つかりますが、自分自身の整理のために書くことにしました。開発環境はUbuntu 22.04LTS、コーディング言語はC++、コンパイラはg++ 11.3.0です。
ライブラリ&ツール群のインストール
HDF5を扱うプログラム開発に必要な諸々は
% apt install libhdf5-dev hdf5-tools
でインストールできます。本記事を書いている時点で、libhdf5のバージョンは1.10.7です。
デフォルト設定で、ヘッダファイル群は /usr/include/hdf5/serial/下、ライブラリ群は /usr/lib/x86_64-linux-gnu/hdf5/serial/lib下にそれぞれ置かれます。C++で開発する場合、includeするヘッダファイルはH5Cpp.h、リンクするライブラリはlibhdf5とlibhdf5_cppの二つです。hdf5-toolsに含まれるシェルスクリプト h5c++ をコンパイラ替わりに使えばこの辺の設定は不要ですが、他のライブラリと組み合わせる際に不便なので知っておく方が良いと思います。本記事を書く際には、私は次のようなmakefileを使ってます。
CXX=g++
INCLUDEDIR=-I/usr/include/hdf5/serial
LIBDIR=-L/usr/lib/x86_64-linux-gnu/hdf5/serial/lib
CXXFLAGS=-Wall -O2 $(INCLUDEDIR) $(LIBDIR)
LINK=-lhdf5_cpp -lhdf5
%: %.cpp
$(CXX) $(CXXFLAGS) $< -o $@ $(LINK)
clean:
rm -f *.o *~
簡単なデータ入出力プログラム
データスペースとデータセット
HDF5ライブラリでは、データはデータスペースとデータセットの組で管理されます。ライブラリではそれぞれH5::DataSpace
、H5::DataSet
というクラスで定義されています。
データスペースはデータの型と配列サイズの情報を持ち、データセットはデータ配列そのものを持ちます。したがって、
- データ出力(書き出し)操作ではデータスペースを定義してからそれと整合するデータセットを作成し、ファイルにデータを転送する
- データ入力(読み込み)操作ではファイルに保存されたデータセットをまず読み込み、それに埋め込まれたデータスペースを取得して、必要なメモリを用意しデータを転送する
という手順にそれぞれなります。
データスペースもデータセットもライブラリ操作のためのインタフェースであって、ファイルフォーマットと直接対応しているわけではないのでご注意下さい。
データ書き出し
まずは簡単なデータwriterを作ります。
#include <iostream>
#include <H5Cpp.h>
#define FILENAME "test.h5"
#define DATASET_NAME "/dataset"
int main(int argc, char *argv[])
{
H5::H5File file( FILENAME, H5F_ACC_TRUNC );
hsize_t dims[] = { 4, 6 };
H5::DataSpace dataspace( sizeof(dims) / sizeof(hsize_t), dims );
H5::DataSet dataset = file.createDataSet( DATASET_NAME, H5::PredType::STD_I32LE, dataspace );
int *dset_data = new int [dims[0]*dims[1]];
if( !dset_data ) return false;
for(int i=0; i<static_cast<int>(dims[0]); i++ ){
for(int j=0; j<static_cast<int>(dims[1]); j++ )
dset_data[i*dims[1]+j] = i * dims[1] + j;
}
dataset.write( dset_data, dataset.getDataType() );
delete dset_data;
dataset.close();
file.close();
return 0;
}
流れを一つ一つ追っていきます。まずは
H5::H5File file( FILENAME, H5F_ACC_TRUNC );
でH5::H5File
インスタンスを作りFILENAME
を読み込んでいます。H5F_ACC_TRUNC
は、もしFILENAME
が存在していたときには上書きする、という指定になります。FILENAME
が存在していたときにはエラーとする場合には、H5F_ACC_EXCL
を第2引数に与えます。
なお、エラーは全て例外として発行されますので、適切に処理するにはtry ... catch を用いる必要があります。これは(その2)で扱うことにします。
次に、データスペースを定義します。
hsize_t dims[] = { 4, 6 };
H5::DataSpace dataspace( sizeof(dims) / sizeof(hsize_t), dims );
dims[]
により、4×6の2次元配列としてデータを保存する、ということを指定しています。コンストラクタの第1引数をsizeof(dims) / sizeof(hsize_t)
とすることで、2次元配列であることを自動的に読み取っています。
そしてファイルに書き出すデータセットを
H5::DataSet dataset = file.createDataSet( datasetname, H5::PredType::STD_I32LE, dataspace );
と作成します。
datasetname
はデータセットの名前(文字列)です。HDFのデータセットは全て文字列でidentifyされます。頭の'/'は省略可能ですが、(その2)で紹介するグルーピングを採用する際にバグの元になるので、慣習として付けることをお勧めします。
第2引数は配列の要素となるデータの型を指定するもので、H5::DataType
のインスタンスを与えます。この例ではその子クラスであるH5::PredType
(PredはPredefinedの略)のstaticメンバSTD_I32LE
を与えています(LEはLittle Endianの意)。
他にどのようなものがあるかは/usr/include/hdf5/serial/H5PredType.hを参照して下さい。例えば同じint型でも、CPUアーキテクチャによってビット幅とendianが違うので、作業が同一マシンで閉じているならばH5::PredType::NATIVE_INT
を使うのが無難です。
続く2重forループで適当に作成したデータ配列dset_data
を、write()メソッドで転送します。
dataset.write( dset_data, dataset.getDataType() );
H5::DataSet
のgetDataType()メソッドは、保存されているデータの型を返すものです。write()メソッドの第2引数になぜもう一度データ型を与えなければならないのかは不明です。
書き出しが終わったらデータセット、ファイルとも閉じましょう。
dataset.close();
file.close();
上記をコンパイルして、コマンドライン引数にファイル名(ここではtest.h5
とします)を与えると、HDF5ファイルが作成されます。hdf5-toolsに含まれるh5dumpを使えば、次のように中身をコンソールに出力することができます。
% h5dump test.h5
HDF5 "test.h5" {
GROUP "/" {
DATASET "dataset" {
DATATYPE H5T_STD_I32LE
DATASPACE SIMPLE { ( 4, 6 ) / ( 4, 6 ) }
DATA {
(0,0): 0, 1, 2, 3, 4, 5,
(1,0): 6, 7, 8, 9, 10, 11,
(2,0): 12, 13, 14, 15, 16, 17,
(3,0): 18, 19, 20, 21, 22, 23
}
}
}
}
データ読み込み
同様にデータreaderも作ってみます。
#include <iostream>
#include <H5Cpp.h>
#define DATASET_NAME "dataset"
int main(int argc, char *argv[])
{
if( argc < 2 ){
std::cerr << "input file name." << std::endl;
return 1;
}
H5::H5File file( argv[1], H5F_ACC_RDONLY );
H5::DataSet dataset = file.openDataSet( argc > 2 ? argv[2] : DATASET_NAME );
file.close();
H5::DataSpace dataspace = dataset.getSpace();
hsize_t dims[dataspace.getSimpleExtentNdims()];
dataspace.getSimpleExtentDims( dims );
int *dset_data = new int [dims[0]*dims[1]];
dataset.read( dset_data, dataset.getDataType() );
for(int i=0; i<static_cast<int>(dims[0]); i++ ){
for(int j=0; j<static_cast<int>(dims[1]); j++ )
std::cout << ' ' << dset_data[i*dims[1]+j];
std::cout << std::endl;
}
delete dset_data;
dataset.close();
return 0;
}
これも流れを一つ一つ追っていきましょう。まずは、
if( argc < 2 ){
std::cerr << "input file name." << std::endl;
return 1;
}
H5::H5File file( argv[1], H5F_ACC_RDONLY );
として、コマンドライン第1引数で指定されたファイルをH5F_ACC_RDONLY
(読み込みモード)で開きます。
読み書き可能にしたい場合はH5F_ACC_RDWR
を第2引数に与えます。これらのファイルアクセスモードを表すマクロは/usr/include/hdf5/serial/H5Fpublic.hで定義されています。
次に
H5::DataSet dataset = file.openDataSet( argc > 2 ? argv[2] : DATASET_NAME );
file.close();
としてファイルに保存されたデータセットを取得しています。openDataSet()メソッドの引数にはデータセット名(文字列)を与えるのですが、ここではコマンドライン第2引数で与えるようにしています(与えられなかった場合、事前定義された"dataset"を使います)。
データセットを取得したらファイルはclose()してしまって構いません。
続いて
H5::DataSpace dataspace = dataset.getSpace();
hsize_t dims[dataspace.getSimpleExtentNdims()];
dataspace.getSimpleExtentDims( dims );
とし、書き出し時とは逆にデータセットからgetSpace()メソッドを使ってデータスペースを取得しています。データスペースのgetSimpleExtentNdims()メソッドは、データ配列の次元数を返すメソッドです。これを使ってhsize_t
の配列を作成し、データスペースのgetSimpleExtendDims()メソッドによってその配列にデータ配列のサイズを抽出します。
得られたデータ配列サイズ情報に基づいてメモリを確保し、データセットのread()メソッドを使って
int *dset_data = new int [dims[0]*dims[1]];
dataset.read( dset_data, dataset.getDataType() );
のようにデータ取得しています。write()メソッドと同様に、read()メソッドも第2引数にデータ型を指定する必要がありますので、getDataType()メソッドを使ってこれを与えます。
その後2重forループで中身を出力した後で、
dataset.close();
としてデータセットを閉じ、終了です。
先程作ったtest.h5
を第1引数に与えて実行すれば、次の出力が得られます。
0 1 2 3 4 5
6 7 8 9 10 11
12 13 14 15 16 17
18 19 20 21 22 23