本記事ではROOTでのデータ保存形式であるTTreeを, ROOTの機能であるRDataFrameを使って利用する方法を解説します.
上記に全て書いてありますが, 一応よく使いそうなケースを示します.
TTreeを読む
ROOTファイルの中身の確認
今, 中身をよく知らないrootファイルがあったとします. 実験をやっていると誰かの作ったrootファイルがあって, 中身についてのDocumentは全然ないということがよくあります. ここでは huge.root
というファイルがあったとします.
最近のROOTには rootls
というコマンドが同梱されています. まずこれで中身を確認します.
> rootls -lt huge.root
TNamed Aug 11 13:39 2022 G4Version;1 " Geant4 version Name: geant4-11-00-patch-02 [MT] (25-May-2022)"
TNamed Aug 11 13:39 2022 git_date;1 "Thu Aug 11 12:18:29 2022"
TNamed Aug 11 13:39 2022 git_sha1;1 "c6c933558eb5a49534b4ee8706f2c9d6df6189c6"
TNamed Aug 11 13:39 2022 git_subject;1 "Add tentative progress"
TNamed Aug 11 13:39 2022 ROOTVersion;1 "6.26/04"
TParameter<long> Aug 11 13:39 2022 seed;1 ""
TTree Aug 11 13:40 2022 tree;10 "mc output" [current cycle]
nHit "nHit/I" 40012851
x "x" 212785592
y "y" 212785592
z "z" 212785592
time "time" 212788101
eIn "eIn" 212787290
eDep "eDep" 212788101
TrackID "TrackID" 176419006
copyNo "copyNo" 176418311
particle "particle" 176419701
Cluster INCLUSIVE ranges:
- # 0: [0, 1011856]
- # 1: [1011857, 2023713]
- # 2: [2023714, 3035570]
- # 3: [3035571, 4047427]
- # 4: [4047428, 5059284]
- # 5: [5059285, 6071141]
- # 6: [6071142, 7082998]
- # 7: [7082999, 8094855]
- # 8: [8094856, 9106712]
- # 9: [9106713, 9999999]
The total number of clusters is 10
TTree Aug 11 13:40 2022 tree;9 "mc output" [backup cycle]
nHit "nHit/I" 36439277
x "x" 193782650
y "y" 193782650
z "z" 193782650
time "time" 193785156
eIn "eIn" 193784346
eDep "eDep" 193785156
TrackID "TrackID" 160664482
copyNo "copyNo" 160663788
particle "particle" 160665176
Cluster INCLUSIVE ranges:
- # 0: [0, 1011856]
- # 1: [1011857, 2023713]
- # 2: [2023714, 3035570]
- # 3: [3035571, 4047427]
- # 4: [4047428, 5059284]
- # 5: [5059285, 6071141]
- # 6: [6071142, 7082998]
- # 7: [7082999, 8094855]
- # 8: [8094856, 9106712]
The total number of clusters is 9
TNamedとか, TParameterなども入っていますが, ここではTTreeに着目します.
tree
という名前のTTreeが入っていることがわかり, その中に nHit
や x
などの変数 (branch) が入っていることがわかります. このファイルが欲しかったBranchがあれば, 次に進みます. そうでなければ, ROOTファイル探しの旅を続けることになります.
これまでは, root -l huge.root
として, ROOTのプロンプトで .ls
する方法が主流でした. 好みの範囲に収まる程度の違いだと思います.
RDataFrameの構築
root
と打ってROOTのPromptを起動して, RDataFrameを作ります.
ROOT::EnableImplicitMT(); // For multi-thread processing
ROOT::RDataFrame df ("tree", "huge.root")
rootマクロとしてではなく, C++としてコンパイルする場合は#include <ROOT/RDataFrame.hxx>
が必要です.
df.Describe()
関数で中身を調べられます.
df.Describe()
(ROOT::RDF::RDFDescription) Dataframe from TChain tree in file huge.root
Property Value
-------- -----
Columns in total 10
Columns from defines 0
Event loops run 0
Processing slots 1
Column Type Origin
------ ---- ------
TrackID ROOT::VecOps::RVec<int> Dataset
copyNo ROOT::VecOps::RVec<int> Dataset
eDep ROOT::VecOps::RVec<double> Dataset
eIn ROOT::VecOps::RVec<double> Dataset
nHit Int_t Dataset
particle ROOT::VecOps::RVec<int> Dataset
time ROOT::VecOps::RVec<double> Dataset
x ROOT::VecOps::RVec<double> Dataset
y ROOT::VecOps::RVec<double> Dataset
z ROOT::VecOps::RVec<double> Dataset
入っている変数の型もここでわかります.
tree->SetBranchAddress()
とか今や必要ありません.これまで必要だった記述
df.GetColumnNames()
でBranch名 (RDataFrameの流儀ではColumn) をリストで受け取ることができるので,
for (auto c : df.GetColumnNames()){
// Something to do
std::cout << c << std::endl;
}
こんな感じでBranch全てに対して行う処理を実行できます.
Treeのお絵描き
まずはヒストグラム
auto h = df.Histo1D("nHit");
h->Draw();
これまで, tree->Draw("nHit")
としていたものの代わりになります.
Cutをかけたければ,
auto h1 = df.Filter("nHit>0").Histo1D("eDep")
とかでFilterしてください.
tree->Draw()
の第二引数にCutを書くより, Filterの結果が使いまわされることに優位性があります.
たくさんTreeを作った場合も, 使う時 (つまり, ポインタにアクセスする際に作成が始まり, まとめられるときは一つのloopで行われます)
ヒストグラムをたくさん定義して,
auto h_x = df.Histo1D("x");
auto h_y = df.Histo1D("y");
auto h_z = df.Histo1D("z");
auto h_eDep = df.Histo1D("eDep");
auto h_eIn = df.Histo1D("eIn");
ヒストグラムを描画します.
h_x->Draw();
この描画しようとした瞬間にevent loopが走ります. 同じevent loopで h_y
なども作られるため, 他のヒストグラムを書くのは爆速です.
df.Describe()
のEvent loops runでloop回数を確認できます. この場合は5個ヒストグラムを作っていますが, event loopは1しか増えていないことが確認できます.
新しいcolumn (branch)の定義
df.Define()
で追加します.
auto df_new = df.Define("new_column", "nHit*2")
nHit*2
の部分はラムダ式や関数を直接入れることが可能.
その場合は第三引数に値のリストを渡す必要がある. {"val1", "val2"}
など
公式のExample
// assuming a function with signature:
double myComplexCalculation(const RVec<float> &muon_pts);
// we can pass it directly to Define
auto df_with_define = df.Define("newColumn", myComplexCalculation, {"muon_pts"});
// alternatively, we can pass the body of the function as a string, as in Filter:
auto df_with_define = df.Define("newColumn", "x*x + y*y");
Treeの保存
これまでの tree->Fill()
, tree->Write()
, file->Close()
を完全に過去にしています.
df_new.Snapshot("new_tree", "new_file.root",
{"new_column", "x", "y", "z"});
これでできてしまいます.
雑感
- 宣言的プログラミングっぽさがあります.
- ROOTはついに “ユーザーは愚かだからLoopを書かせない” という進化をしてきているのかもしれません.
- 煩雑なTTreeの手続きがかなり楽になっています. Branchに文字列でアクセスすることができるだけでかなり幸せです.
- 真面目に使おうとするとラムダ式とauto盛り盛りになるので, C++11に追いついてきていないユーザーには意味不明なコードになるでしょう.
- printf デバッグをしてきた人は大変に, Unitテストを書く側からするとより楽になる気がします.