TTree是ROOT的一个存储数据的类,大概可以理解成一个二维数据表格。每一个Branch代表一种属性(或者一列),Entry代表一个事例(或者一行)。用户可以通过读取每一Entry中的特定Branch,来统计一个样本的某个属性的分布。
读取TTree数据的方法有很多,这里总结几个比较常用的,他们有的比较复杂但比较灵活,有的简单快捷,但是面对更复杂的数据结构或者操作就无能为力。不同的场景适用不同的方法,但个人觉得RDataFrame在各种情况都相对好用一些,写这篇博客也是起因于此。
其实可以直接去看文档
https://root.cern/manual/trees/#showing-an-entry-of-a-tree
https://root.cern/doc/master/tree2_8C.html
https://root.cern/doc/master/classTTree.html#a73450649dc6e54b5b94516c468523e45
https://root.cern/doc/master/classROOT_1_1RDataFrame.html
利用TTree本身的成员方法
以下内容大部分来自于官方文档的这几个部分:
https://root.cern/manual/trees/#showing-an-entry-of-a-tree
https://root.cern/doc/master/tree2_8C.html
https://root.cern/doc/master/classTTree.html#a73450649dc6e54b5b94516c468523e45
从一个.root
文件中读取TTree的方法:
TFile *f = new TFile("tree2.root");
TTree *t2 = (TTree*)f->Get("t2");
其中TFile的构造函数打开了一个.root
文件,Get方法从文件中找到名字对应的TObject,转换成TTree之后即可拿到一个TTree的指针。
TTree::Draw
TTree内置的Draw方法可以快捷的统计某个Branch或者某几个Branch的分布,还可以设置筛选条件,以及画图选项。
例如我们有一个Tree,其中包含这些Branch:
root [3] pion->Print()
******************************************************************************
*Tree :pion : pion *
*Entries : 14228 : Total = 860959 bytes File Size = 553940 *
* : : Tree compression factor = 1.55 *
******************************************************************************
*Br 0 :RunNum : RunNum/I *
*Entries : 14228 : Total Size= 57547 bytes File Size = 547 *
*Baskets : 2 : Basket Size= 32000 bytes Compression= 104.31 *
*............................................................................*
*Br 1 :PiNumInEvent : PiNumInEvent/I *
*Entries : 14228 : Total Size= 57583 bytes File Size = 7760 *
*Baskets : 2 : Basket Size= 32000 bytes Compression= 7.35 *
*............................................................................*
*Br 2 :EventNum : EventNum/I *
*Entries : 14228 : Total Size= 57559 bytes File Size = 17574 *
*Baskets : 2 : Basket Size= 32000 bytes Compression= 3.25 *
*............................................................................*
*Br 3 :MCPEn : MCPEn/D *
*Entries : 14228 : Total Size= 114605 bytes File Size = 88737 *
*Baskets : 4 : Basket Size= 32000 bytes Compression= 1.29 *
*............................................................................*
*Br 4 :MCPTheta : MCPTheta/D *
*Entries : 14228 : Total Size= 114629 bytes File Size = 89337 *
*Baskets : 4 : Basket Size= 32000 bytes Compression= 1.28 *
*............................................................................*
*Br 5 :MCPPhi : MCPPhi/D *
*Entries : 14228 : Total Size= 114613 bytes File Size = 90367 *
*Baskets : 4 : Basket Size= 32000 bytes Compression= 1.26 *
*............................................................................*
*Br 6 :DauPhotonEnMax : DauPhotonEnMax/D *
*Entries : 14228 : Total Size= 114677 bytes File Size = 86169 *
*Baskets : 4 : Basket Size= 32000 bytes Compression= 1.32 *
*............................................................................*
*Br 7 :DauPhotonEnMin : DauPhotonEnMin/D *
*Entries : 14228 : Total Size= 114677 bytes File Size = 86793 *
*Baskets : 4 : Basket Size= 32000 bytes Compression= 1.32 *
*............................................................................*
*Br 8 :DauPhotonCluDis : DauPhotonCluDis/D *
*Entries : 14228 : Total Size= 114685 bytes File Size = 85405 *
*Baskets : 4 : Basket Size= 32000 bytes Compression= 1.34 *
*............................................................................*
统计其中“MCPID”的分布:
pion->Draw("MCPEn")
用一个Branch做筛选
pion->Draw("MCPEn","MCPEn != 0")
这些都比较简单,文档里都很好找。还可以用TBrowser在交互式界面中去观察数据。
可以通过gDirectory
拿到这个直方图。
pion->Draw("DauPhotonCluDis>>h(40,0,200)");
TH1D* h = (TH1D*)gDirectory->FindObject("h");
如果要处理的Branch对应的数据类型是基本的double,int等,那么以上方法非常简便。
如果处理的Branch对应的是一个容器,比如vector,甚至是一个用户自定义的结构体或对象。Draw()的能力就比较有限。
Draw函数提供了一些基本的操作来访问所存储的vector的属性。例如这样一个Tree
root [2] Event->Show(0)
======> EVENT:0
RunNum = 1
EventNum = 0
MCPEn = 2.8601
MCPTheta = 2.46585
MCPPhi = 1.49832
Conv = 0
LeadingEn = 1.85087
SubLeadingEn = (vector<double>*)0x7fffbc4e5700
SubLeadingEn
是一个vector<double>
的Branch,每个vector都有一个size()方法,可以通过@SubLeadingEn.size()
的方式统计这个方法的返回值:
Event->Draw("@SubLeadingEn.size()")
这样也是可以解决一部分问题。但如果要进行更复杂的操作,比如说统计SubLeadingEn这个vector中所有不为零的项目的个数,或者两个等长vector的Branch(A,B),把B按照A的降序排列,统计A最大的那个元素所对应的B元素,或者计算所有非零A元素对应的B元素之和,这个时候上述方法就会比较麻烦。
TBranch::GetEntry
这是一个非常通用的办法,也是我第一次接触ROOT所见到的方法。
它的逻辑大概是这样的:
- 获取需要的Branch:
GetBranch
- 为处理的Branch都指定一个对应数据类型的地址:
SetAddress
- 遍历所有Entries,每经过一个Entry,对应地址的数据就刷新一次:
GetEntry(i)
下面是官方教程给出的示例:
TFile *f = new TFile("tree2.root");
TTree *t2 = (TTree*)f->Get("t2");
static Float_t destep;
TBranch *b_destep = t2->GetBranch("destep");
b_destep->SetAddress(&destep);
//create one histogram
TH1F *hdestep = new TH1F("hdestep","destep in Mev",100,1e-5,3e-5);
//read only the destep branch for all entries
Long64_t nentries = t2->GetEntries();
for (Long64_t i=0;i<nentries;i++) {
b_destep->GetEntry(i);
hdestep->Fill(destep); // 注意这句要放在循环里
}
// hdestep->Fill(destep); // 而不能放在循环外,否则TH1F只Fill到最后一个数据
这个方法几乎可以满足所有的需求,无论是处理复杂的对象还是跨Entries处理数据,都是用户自己定义的。缺点就是代码太长了,要定义的东西太多了,需求稍有变化,代码就要大改,而且可读性不好,经常多个循环嵌套在一起,时间长了不知道自己写的是什么。
RDataFrame
https://root.cern/doc/master/classROOT_1_1RDataFrame.html
RDataFrame是ROOT命名空间的一个类,它(以及其他配和使用的类一起)构成一套比较整洁的数据处理模式。GetEntry的方法就像一个手工作坊,每一步都要亲自一步一步做好。RDataFrame有自己的多个车间,每个车间都可以接收一个半成品,加工之后送给下一个车间,用户只需要把车间顺序安排好,把标准设定好,其中很多原本要设置的中间变量都可以放下不管。这样代码的思路就非常清晰,修改的时候也会比较简单。
在这之前先设置好命名空间:
using namespace ROOT;
从.root
文件中获取一个Tree,填入RDataFrame:
RDataFrame df("Event","tautau.root");
对其中的事例进行筛选,可以使用Filter方法:
double en_threshold = 0.3;
double acceptance = 0.99;
auto filter_en = [&en_threshold, &acceptance](double t, double r, double theta)
{
return (t > en_threshold || t == 0) && (fabs(TMath::Cos(theta)) < acceptance || t == 0);
};
auto define_df = df.Filter(filter_en,{"MCPEn","RecoPEn","MCPTheta"});
其中你需要给Filter一个筛选的判据,即Lambda表达式filter_en
,然后告诉Fitler需要用到的信息来自于哪些Branch{"MCPEn","RecoPEn","MCPTheta"}
。这样,Fiter会返回一个对象ROOT::RDF::RInterface
,这个类型不需要太在意,总之就是类似与一个处理过的DataFrame,可以用auto
来承接这个返回值,也可以用ROOT::RDF::RNode
。
此外,也可以使用类似TTree::Draw()的写法,输入一条字符串来完成筛选。多个Filter可以叠加在一起,还可以为每一个Filter命名用以区分。最后,可以获得这个Filter的效率:
auto cut_df = df.Filter("Num_Pi0 >= 0", "1 Pion")
.Filter("newRecoPID != 0 || MCPID != 0","ToF cut");
cut_df.Report()->Print();
输出如下:
1 Pion : pass=875634 all=875634 -- eff=100.00 % cumulative eff=100.00 %
ToF cut : pass=875634 all=875634 -- eff=100.00 % cumulative eff=100.00 %
这个Filter的好处在于,它几乎可以完成任何你想完成的操作,因为Lambda表达式相当于一个代码段。同时它又非常直观,你不需要去考虑处理每个Entry的时候要把数据放在那里,判据不是分散在循环中,而是独立存在于一个Lambda表达式中,如果需要修改代码,你可以一目了然地找到定义判据的位置。(当然,如果coding习惯很好,把每个代码段都包装得很规范,也可以用GetEntry达到相同的效果,但相对就还是稍微麻烦一点。)
这里Filter的判据可以使用Lambda表达式或者函数对象。用Lambda的一个好处在于它有变量捕获的功能,可以抓取外部变量,也是相对比较整洁。
这个Filter是一个模板,文档里是这么写的:template<typename Proxied , typename DataSource = void> template<typename F , std::enable_if_t<!std::is_convertible< F, std::string >::value, int > = 0> RInterface<RDFDetail::RFilter<F, Proxied>, DS_t> ROOT::RDF::RInterface< Proxied, DataSource >::Filter ( F f, const ColumnNames_t & columns = {}, std::string_view name = "" )
Append a filter to the call graph.
Parameters
[in] f Function, lambda expression, functor class or any other callable object. It must return a bool signalling whether the event has passed the selection (true) or not (false).
[in] columns Names of the columns/branches in input to the filter function.
[in] name Optional name of this filter. See Report.
要从已知的数据中定义新的数据,比如说从动量能量信息计算质量,可以用Define方法。这里还是用到Lambda表达式举例:
auto cut_df = df.Filter("Num_Pi0 >= 0", "1 Pion")
.Filter("RecoPID != 0 || MCPID != 0","ToF cut");
auto paired_photon = cut_df.Filter("MCPID == 22 && RecoPID == 22","Paired");
double tof_smear = 0;
TRandom3 rdm_generator;
/************ TOF distribution **************/
double tof_min = -6;
double tof_max = 2;
int nbins_tof = 100;
auto paired_tof = paired_photon.Define("ToF_smear", [&tof_smear, &rdm_generator](double tof){ return (rdm_generator.Gaus(tof, tof_smear)); },{"RecoPTOF"})
.Define("logTOF","log10(ToF_smear)")
.Histo1D({"","paired photons",nbins_tof,tof_min,tof_max},"logTOF");
到paired_photon
之前,都是叠加多个Filter,然后在paired_photon
的基础上使用Define,首先告诉Define方法,想要定义的这个新的量叫什么(“ToF_smear”),然后用“lambda, branch name list”来告诉Define,对RecoPTOF
这个量加一个高斯的smear。这类似于Filter,只不过Filter要求输入一个bool返回值的函数对象或lambda,而Define要求返回一个double或者int之类的变量。
此外,上面的代码段在Define之后还调用了Histo1D来生成直方图,除了Histo1D还有Histo2D,Profile1D,Count,Sum,Foreach等等统计工具可以使用。官方文档里讲的听清楚的,这里只是举例解释一下它的逻辑,以及展示一下它还是挺好用的。目前来讲我不是很喜欢GetEntry的方法,用RDataFrame可以完全取代。
RDataFrame这一套工具有很鲜明的现代C++的编程风格,广泛使用模板使他的很多函数可以处理不同类型的数据,并且一定程度上接近python弱类型语言,你不需要关心它给你的东西是什么类型(函数传参的时候可以统一用ROOT::RDF::RNode
来代替RDataFrame
和RInterface
)。
总之就是挺方便的,精力有限~~(懒)~~ ,先草草说这么几句,官网上讲的很全,还有很多有意思的功能(比如转化成numpy,开启多线程等)以及配套的示例,没事可以去翻翻。