1.1 用TTree 结构存储事件数据¶
1. 目的¶
- 理解核物理实验中“事件(Event)”的概念。
- 掌握如何利用 Monte Carlo 方法模拟粒子探测过程。
- 学习将数据以 TTree 格式存储到 ROOT 文件中。
2、 实验设置与物理原理¶
束流入射到靶上产生核反应,生成中子和$\gamma$ flash。这些次级粒子飞行一段距离后,击中一个长条型塑料闪烁体探测器。
2.1. 几何设置¶
- 束流:沿 $y$ 轴射入,打在靶点 ($0,0$) 上。
- 探测器:长条形塑料闪烁体。
- 位置:中心位于 $(0, D)$,即距靶点垂直距离为 $D$。
- 尺寸:长 $2L$,厚度 $T$(忽略宽度)。
- 坐标系:探测器沿 $X$ 轴放置。粒子击中位置 $x \in [-L, L]$。
- 粒子可能在探测器厚度范围内任意深度 $y' \in [-T/2, T/2]$ 发生反应。
2.2. 信号读出¶
探测器两端分别用光电倍增管(PMT)读出信号,定义为 Left (图中u) 和 Right (图中d) 端。
- Time:记录信号到达时间 ($t_L, t_R$)。
- Charge:记录沉积能量 ($q_L, q_R$)。
2.3. 物理公式¶
A. 飞行时间 (TOF)
粒子从靶点飞到反应点 $(x,D+y')$ 的真实距离: $$ d = \sqrt{x^2 + (D+y')^2} $$
- Gamma ($\gamma$):以光速 $c$ 飞行。 $$ \text{TOF}_\gamma [\text{ns}]\approx 3.33* d(\text{m}) $$
- 中子 ($n$):非相对论近似,能量 $E_n$ (MeV)。 $$ \text{TOF}_n [\text{ns}] \approx 72.3 \times \frac{d [\text{m}]}{\sqrt{E_n [\text{MeV}]}} $$
B. 探测器时间响应
光在闪烁体中传输速度为 $v_{sc}$。
$$ t_L = \text{TOF} + \frac{L+x}{v_{sc}} + t_{0L} + \text{Gaus}(0, \sigma_{t_L}) $$ $$ t_R = \text{TOF} + \frac{L-x}{v_{sc}} + t_{0R} + \text{Gaus}(0, \sigma_{t_R})$$
其中 $t_{0L}$,$t_{0R}$:读出各通道的所有固定时间延迟(线缆、PMT 渡越时间、电子学固有延迟);$\sigma_t$为时间分辨率 。
C. 探测器能量响应
中子探测中的能量转换: 在塑料闪烁体探测中子时,由于中子不带电,其探测过程主要依赖于中子与闪烁体内氢原子核的弹性散射($(n, p)$ 散射)。其能量响应过程可分为以下几个阶段:
能量传递过程 入射中子的能量通过碰撞部分地传递给闪烁体内的质子。由于中子与质子的质量几乎相等,在一次弹性散射中,反冲质子获得的最大能量 $E_{p\_max}$ 等于中子的入射能量 $E_n$: $$E_{pmax} = E_n$$ 在质心系各向同性散射的近似下,反冲质子的能量 $E_p$ 在 $0$ 到 $E_{p\_max}$ 之间呈均匀分布。质子随后在闪烁体内通过电离和激发过程损失能量,并最终转化为闪烁光信号。
电离猝灭效应(Quenching Effect) 由于闪烁体的发光过程存在“电离猝灭效应”,其发光强度具有明显的粒子相关性。对于具有相同沉积能量的不同粒子,质子产生的总光量远小于电子。 为了定量描述这种差异,实验上引入了电子等效能量 $E_{ee}$ (MeVee) 的概念。在该定义下,闪烁体的发光强度与该粒子的 $E_{ee}$ 成正比(即 $1 \text{ MeV}$ 的电子产生的发光量定义为 $1 \text{ MeVee}$)。
0-50 MeV 质子的经验响应模型 对于 0-50 MeV 范围内的反冲质子,其电子等效能量 $E_{ee}$ 与质子实际沉积能量 $E_p$ 之间的转换遵循如下经验关系(Cecil 公式): $$E_{ee} = a E_p - b \left[ 1 - \exp(-c E_p^d) \right]$$ 根据对塑料闪烁体(如 BC408/NE-102)的大量实验数据拟合,标准建议参数为: $a = 0.83, b = 2.82, c = 0.25, d = 0.93$
闪烁体发光强度
$$ Q \propto E_{ee}$$
光传输遵循指数衰减,衰减长度为 $\lambda$。 $$ q_L = Q/2 \cdot e^{-(L+x)/\lambda} + \text{Gaus}(0,\sigma_{qL}) $$ $$ q_R = Q/2 \cdot e^{-(L-x)/\lambda} + \text{Gaus}(0,\sigma_{qR})$$ 其中 $\sigma_{q_{L/R}} = q_{L/R}\cdot R/2.355$, $R=\Delta E/E = 2.355\cdot \sigma_E /E$ 为相对能量分辨率 。
在闪烁体探测器系统中,实验测量到的信号电荷量并非直接等于物理上的发光量 $q_L$ 和 $q_R$,而是经过了光电倍增管(PMT)的增益和后端读出电路(放大器、ADC等)的转换。
为了从测量值中还原出能量信息 $Q$,需要在原有的物理模型中加入刻度系数。
实际观测到的实验信号 $A_L$ 和 $A_R$(例如 ADC 道址)与理论光量 $q_L$、$q_R$ 的关系为: $$ A_L = g_L \cdot q_L $$ $$ A_R = g_R \cdot q_R $$
D. 阈值 (Threshold):
设置电子学阈值 $A_{th}$。仅当两端信号幅度均超过阈值($A_L>A_{th}$ 和 $A_R>Q_{th}$)时,该事件被记录。
E. 时间分辨与信号幅度的关系
闪烁体+PMT的时间分辨率 $\sigma_t$ 并不是一个常数,而与信号幅度的大小相关。一般情况下幅度越小,时间分辨越差。
经验描述公式为: $$\sigma_{t}^2 = \sigma_{\text{floor}}^2 + \frac{\sigma_{\text{stat}}^2}{A} + \frac{\sigma_{\text{noise}}^2}{A^2}$$
- $\sigma_{\text{floor}}$ (固有项):系统能达到的极限分辨,由 PMT 渡越时间散布 (TTS) 和电子学精度决定。
- $\sigma_{\text{stat}}/\sqrt{A}$ (统计项):
- $\sigma_{\text{noise}}/A$ (电子学抖动项):
E. Time-Walk Effect
原理: 在实验中,电子学通常使用固定阈值甄别器 (Leading Edge Discriminator) 来确定信号到达的时间。即当模拟信号的幅度超过设定的阈值电压 ($V_{th}$) 时,逻辑信号被触发。 由于探测器输出信号有一定的上升时间 (Rise Time),大幅度信号比小幅度信号更早达到阈值。 这种信号幅度 ($A$) 对测量时间 ($t$) 的影响称为 Time-Walk。
模拟公式: 通常可以用与电荷量相关的反比函数来近似这种延迟: $$t_{walk} \approx \frac{W}{\sqrt{A}}$$ 其中 $W_A$ 是与电子学上升时间相关的常数。
F. 最终测量时间
在真实的核物理实验中,探测器两端的光电倍增管(PMT)和电子学逻辑通常存在性能差异。这意味着左端(Left)和右端(Right)的 Time-Walk 系数 $A_L$ 与 $A_R$ 并不相等。 综合考虑理想时间、时间分辨与幅度的关系以及walk效应,最终记录到 Tree 中的时间为: $$\sigma_{t_{L/R}} = \sqrt{\sigma_{floor}^2 + \sigma_{stat}^2/A_{L/R}+ \sigma_{niose}^2/A_{L/R}^2} $$ $$t_{mes_{L/R}} = t_{L/R} + \frac{W_{A_{L/R}}}{\sqrt{A_{L/R}}} + \text{Gaus}(0, \sigma_{t_{L/R}})$$
G. 重构参数 (Reconstruction)
利用时间信息 $t_L, t_R$ 反推位置和飞行时间: $$ x_{t} = \frac{v_{sc}}{2} (t_L - t_R - (t_{0L}-t_{0R}))$$ $$ \text{TOF}_{rec} = \frac{t_L + t_R - (t_{0L}+t_{0R})}{2} -L/v_{sc}$$
利用能量信息 $q_L, q_R$ 反推原始能量沉积和位置: $$ x_q = \frac{\lambda}{2}\ln \frac{q_L}{q_R} $$ $$Q_0^2 = e^{2L/\lambda} q_L \cdot q_R/4$$
3. ROOT 文件结构 (TTree) — 数据生成与存储机制¶
在处理核物理实验数据时,数据量通常达到百万甚至千万量级。ROOT 框架的 TTree 结构实现了高效的存储。
3.1 TTree的数据存储¶
以下代码展示了从内存变量定义到磁盘文件落盘的完整生命周期:
// ==========================================
// [初始化阶段] 环境建立与地址绑定
// ==========================================
//建立 ROOT 文件 (TFile)
//创建 数据树 (TTree)
// 定义本地工作区变量(数据中转站)
Int_t val_pid;
Double_t val_e_true, val_tL, val_tR; // ... 其他变量
// 【建立地址映射】
tree->Branch("pid", &val_pid, "pid/I");
tree->Branch("e_true", &val_e_true, "e_true/D");
tree->Branch("tL", &val_tL, "tL/D");
tree->Branch("tR", &val_tR, "tR/D");
// ==========================================
// [事件循环阶段] 数据刷新与快照填充
// ==========================================
for (从 0 到 总事件数) {
1. 计算/获取逻辑:更新内存变量的内容
val_pid = 获取粒子ID;
val_e_true = 获取模拟能量;
val_tL = 计算左端响应时间;
val_tR = 计算右端响应时间;
2. 填充事件快照:
tree->Fill();
}
//保存数据到磁盘
tree->Write(); // 将缓冲区内的所有事件批量压缩并写入磁盘
file->Close(); // 关闭文件,安全释放内存连接
这里是对 TTree 核心概念的澄清与简化版本,去除了晦涩的底层术语,直接回归 ROOT 官方文档中最核心、最实用的操作逻辑:
3.2 TTree 的 Branch 接口参数 (基础变量)¶
创建一个分支并将其与内存中的变量绑定:
tree->Branch("BranchName", &Variable, "LeafName/Type");
- 参数 1:
"BranchName":保存在 ROOT 文件中的分支名称。 - 参数 2:
&Variable:变量的内存地址。ROOT 将通过这个地址来读取数据。 - 参数 3:
"LeafName/Type":数据类型描述符,告诉 ROOT 如何读取该内存地址。- LeafName:数据的真实字段名。在进行绘图(如 tree->Draw("LeafName"))或条件筛选时使用。建议保持与 BranchName 一致,以避免混淆。
- Type:
/D:Double_t(64位浮点)
/F:Float_t(32位浮点)
/I:Int_t(32位整型)
/L:Long64_t(64位整型)
3.3 TTree 核心操作机制¶
1. 地址绑定 (一次性连接)
Branch() 的作用仅仅是让 TTree “记住”变量的内存地址。因此,声明分支的操作只需要在循环外执行一次。在循环内更新变量的值。
2. 数据快照 (Fill 的作用)
tree->Fill() 相当于按下相机快门。每次调用时,TTree 会去读取之前绑定的所有内存地址,把当时的当前数值拷贝一份,保存为 TTree 中的一个新“事件”(Entry)。
3. 内存缓冲 (Basket) 与 批量写入
为了提高性能,Fill() 不会立刻把数据写到硬盘上。数据会被先塞进内存里的暂存区(称为 Basket)。只有当 Basket 装满时,ROOT 才会自动将其压缩并批量写入磁盘。这极大地减少了磁盘读写(I/O)的次数。
3.4 存入TTree的事件信息¶
将入射粒子的原始信息以及探测器响应信息逐事件同步存入 TTree 中:
- 物理信息(源数据):
- 粒子种类 (
pid) - 真实能量 (
e_true) - 入射位置 (
x_true) - 入射深度 (
z_true) - 真实飞行时间 (
tof_true)
- 粒子种类 (
- 探测器信息(响应数据):
- 两端时间信息:$t_L, t_R$
- 能量沉积(电荷)信息:$q_L, q_R$
3. 模拟代码¶
说明:为了演示核心逻辑,此代码对物理过程进行了简化:
- 暂不考虑中子反冲质子的能量分布,直接假设闪烁体能量沉积 $Q_0 = E_{n_0}$。
- 暂不设置电子学阈值,所有事件均被记录。 (这些内容将在作业中完成)
void create_tree(){
// 1. 常量声明
const Int_t nEvents = 500000;
const Double_t D = 5.0; // m, 靶到探测器距离
const Double_t L = 1.0; // m, 探测器半长
const Double_t T = 0.05; // m, 厚度
const Double_t Lambda = 3.8; // m, 衰减长度
const Double_t Vsc = 0.075; // m/ns, 光速
const Double_t a = 0.83;
const Double_t b = 2.82;
const Double_t c = 0.25;
const Double_t d = 0.93;
const Double_t En0 = 100.0; // MeV, 中子平均能量
const Double_t EnFWHM = 50.0; // MeV, 中子能量展宽
const Double_t Egmean = 15.0; // MeV, Gamma能量
const Double_t RatioGamma = 0.3;
const Double_t sigt_floor = 0.5; // 高能端极限 sigma (ns)
const Double_t sigt_no = 5; // noise fluctuation
const Double_t sigt_stat = 2.0; // 统计项系数 (ns*sqrt(ADC))
const Double_t Walk_AL = 40.0; // ns*sqrt(q), Walk系数: t = t_true + A/sqrt(Q)
const Double_t Walk_AR = 45.0; // ns*sqrt(q), Walk系数: t = t_true + A/sqrt(Q)
const Double_t gL_qe = 10; //ee to light;
const Double_t gR_qe = 15; //ee to light;
const Double_t Rq = 0.1; // 相对能量分辨率
const Double_t t0L = 5.5;
const Double_t t0R = 20.4;
const Double_t A_th = 5; // 阈值
// 2. 声明 Tree 中的变量
Double_t x_true; // 真实入射位置 (m)
Double_t e_true; // 真实能量
Int_t pid; // 0: Gamma, 1: Neutron
Double_t tof_true;// 真实 TOF (ns)
Double_t tL_true, tR_true; // 探测器时间
Double_t tL, tR; // 探测器时间
Double_t qL, qR; // 探测器能量
Double_t AL, AR; // 探测器能量
// 3. 定义文件与 Tree
TFile *opf = new TFile("tree.root", "recreate");
TTree *opt = new TTree("tree", "Simulated Data");
opt->Branch("x_true", &x_true, "x_true/D");
opt->Branch("e_true", &e_true, "e_true/D");
opt->Branch("tof_true", &tof_true, "tof_true/D");
opt->Branch("pid", &pid, "pid/I");
opt->Branch("tL_true", &tL_true, "tL_true/D");
opt->Branch("tR_true", &tR_true, "tR_true/D");
opt->Branch("tL", &tL, "tL/D");
opt->Branch("tR", &tR, "tR/D");
opt->Branch("AL", &AL, "AL/D");
opt->Branch("AR", &AR, "AR/D");
TH1D *htof = new TH1D("htof", "Reconstructed time of flight", 1000, 0, 100);
TRandom3 *gr = new TRandom3(0);
std::cout << "Starting Simulation..." << std::endl;
// 4. 事件循环
for(int i = 0; i < nEvents; i++){
// --- 进度条提示输出 (每5%刷新一次) ---
if (i % (nEvents / 20) == 0) {
Double_t progress = (Double_t)i / nEvents * 100;
std::cout << "\rProcessing: " << (int)progress << "% completed..." << std::flush;
}
// --- 生成真值 ---
x_true = gr->Uniform(-L, L);
Double_t y_prime = gr->Uniform(-T/2.0, T/2.0); // 深度 y'
Double_t dis = TMath::Sqrt(pow(D + y_prime, 2) + pow(x_true, 2)); // 真实距离
Double_t E_dep = 0; // 沉积能量 (产生光信号的能量)
if(gr->Uniform() < RatioGamma) { // Gamma
pid = 0;
e_true = gr->Exp(Egmean);
tof_true = 3.333 * dis; // m -> ns
E_dep = e_true;// Gamma沉积能量
} else { // Neutron
pid = 1;
e_true = En0; //中子能量,
if(e_true <= 0) continue;
tof_true = 72.3 / TMath::Sqrt(e_true) * dis ;
E_dep = e_true;//中子能量沉积
}
// --- 探测器响应 ---
// 时间信号
tL_true = tof_true + (L + x_true)/Vsc + t0L; // x 定义方向
tL = gr->Gaus(tL_true, sigt_floor);
tR_true = tof_true + (L - x_true)/Vsc + t0R;
tR = gr->Gaus(tR_true, sigt_floor);
// 能量信号
// 此处简化:暂不考虑反冲质子能量分布
Double_t Q = E_dep;
qL = (Q/2.0) * TMath::Exp(-(L + x_true)/Lambda);
qL = gr->Gaus(qL, qL * Rq/2.35);
AL = gL_qe * qL;
qR = (Q/2.0) * TMath::Exp(-(L - x_true)/Lambda);
qR = gr->Gaus(qR, qR * Rq/2.35);
AR = gR_qe * qR;
if (AL < 0 || AR < 0) continue;
// 在线重构
double tof_rec = (tL + tR)/2.0;
htof->Fill(tof_rec); //填充histogram
//5.将计算好的变量值填到Tree中
opt->Fill();
}
// 完成后输出 100%
std::cout << "\rProcessing: 100% completed! " << std::endl;
// 6.将数据写入root文件中
htof->Write();
opt->Write();
opf->Close();
std::cout << "Data saved to tree.root" << std::endl;
}
4. 运行与分析¶
ROOT终端运行方法:
- 保存代码为
create_tree.cc。 - 在终端输入
root -l create_tree.cc(自动执行并退出),生成tree.root。 - 打开文件:
root -l tree.root。 - 显示文件结构:
.ls - 查看Tree结构:
tree->Print(); - ...
create_tree();
Starting Simulation... Processing: 100% completed! Data saved to tree.root
练习:¶
- 理解上述1.-6.步骤的逻辑关系。自行练习如何将数据以TTree格式存到ROOT文件中。
- 理解如何在模拟中加入探测器分辨。
4.1. 查看ROOT文件¶
//%jsroot on
TFile *ipf = new TFile("tree.root");//root -l tree.root
ipf->ls()//ROOT 环境下 > .ls
TFile** tree.root TFile* tree.root KEY: TTree tree;2 Simulated Data [current cycle] KEY: TTree tree;1 Simulated Data [backup cycle] KEY: TH1D htof;1 Reconstructed time of flight
//观察tree的结构
tree->Print()
****************************************************************************** *Tree :tree : Simulated Data * *Entries : 500000 : Total = 38111950 bytes File Size = 30674091 * * : : Tree compression factor = 1.24 * ****************************************************************************** *Br 0 :x_true : x_true/D * *Entries : 500000 : Total Size= 4011942 bytes File Size = 2797027 * *Baskets : 125 : Basket Size= 3368960 bytes Compression= 1.43 * *............................................................................* *Br 1 :e_true : e_true/D * *Entries : 500000 : Total Size= 4011942 bytes File Size = 1437266 * *Baskets : 125 : Basket Size= 3368960 bytes Compression= 2.79 * *............................................................................* *Br 2 :tof_true : tof_true/D * *Entries : 500000 : Total Size= 4012200 bytes File Size = 3556188 * *Baskets : 125 : Basket Size= 3368960 bytes Compression= 1.13 * *............................................................................* *Br 3 :pid : pid/I * *Entries : 500000 : Total Size= 2005967 bytes File Size = 211716 * *Baskets : 63 : Basket Size= 1684480 bytes Compression= 9.47 * *............................................................................* *Br 4 :tL_true : tL_true/D * *Entries : 500000 : Total Size= 4012071 bytes File Size = 3773792 * *Baskets : 125 : Basket Size= 3368960 bytes Compression= 1.06 * *............................................................................* *Br 5 :tR_true : tR_true/D * *Entries : 500000 : Total Size= 4012071 bytes File Size = 3762401 * *Baskets : 125 : Basket Size= 3368960 bytes Compression= 1.07 * *............................................................................* *Br 6 :tL : tL/D * *Entries : 500000 : Total Size= 4011337 bytes File Size = 3774018 * *Baskets : 124 : Basket Size= 3368448 bytes Compression= 1.06 * *............................................................................* *Br 7 :tR : tR/D * *Entries : 500000 : Total Size= 4011337 bytes File Size = 3764227 * *Baskets : 124 : Basket Size= 3368448 bytes Compression= 1.06 * *............................................................................* *Br 8 :AL : AL/D * *Entries : 500000 : Total Size= 4011337 bytes File Size = 3795368 * *Baskets : 124 : Basket Size= 3368448 bytes Compression= 1.06 * *............................................................................* *Br 9 :AR : AR/D * *Entries : 500000 : Total Size= 4011337 bytes File Size = 3792262 * *Baskets : 124 : Basket Size= 3368448 bytes Compression= 1.06 * *............................................................................*
4.2 查看事件数据¶
tree->Show(10);//显示指定的事件数据 event=10
======> EVENT:10 x_true = -0.746245 e_true = 100 tof_true = 36.644 pid = 1 tL_true = 45.5274 tR_true = 80.3272 tL = 46.0796 tR = 80.2509 AL = 470.406 AR = 500.379
tree->Scan("x_true:e_true:tof_true:pid:tL:tR:AL:AR","","",10,10);
************************************************************************************************************ * Row * x_true * e_true * tof_true * pid * tL * tR * AL * AR * ************************************************************************************************************ * 10 * -0.746245 * 100 * 36.643961 * 1 * 46.079593 * 80.250888 * 470.40624 * 500.3791 * * 11 * -0.926384 * 100 * 36.659505 * 1 * 43.760717 * 83.046770 * 479.98667 * 430.03453 * * 12 * 0.0953839 * 100 * 36.210595 * 1 * 55.893525 * 69.558013 * 394.79357 * 591.15340 * * 13 * 0.3093909 * 100 * 36.176069 * 1 * 59.183832 * 66.666512 * 392.44258 * 641.17039 * * 14 * -0.958320 * 100 * 36.950511 * 1 * 42.570520 * 83.084246 * 519.85725 * 441.99134 * * 15 * -0.760824 * 100 * 36.399884 * 1 * 45.660114 * 80.248237 * 453.88114 * 418.91337 * * 16 * -0.018517 * 4.0821660 * 16.627294 * 0 * 35.755004 * 51.052217 * 16.760023 * 23.442220 * * 17 * -0.632558 * 100 * 36.430165 * 1 * 45.983321 * 78.262556 * 448.97570 * 466.07457 * * 18 * -0.143903 * 100 * 36.278417 * 1 * 53.069224 * 71.807450 * 396.81997 * 580.17878 * * 19 * 0.2871965 * 100 * 36.094508 * 1 * 59.458789 * 67.066652 * 353.29347 * 647.69140 * ************************************************************************************************************
4.3 查看相对位置分布¶
TCanvas *c1 = new TCanvas();//* 在ROOT环境下可省略
c1->Clear();//* 在ROOT环境下可省略
tree->Draw("tR-tL>>(100,-15,50");
c1->Draw(); //* 在ROOT环境下可省略
4.4:TOF¶
- 观察以下两图有什么不同,分析其原因
tree->Draw("tof_true>>(1000,0,100)","");//实际飞行时间
c1->Draw();
TH1D *hh = (TH1D*) ipf->Get("htof"); //在代码中得到文件内数据指针的方法,在ROOT环境下课直接用 htof->Draw()
hh->Draw();//画图
c1->Draw();
4.5 飞行时间与位置的二维关联¶
- 右图:飞行距离修正,将TOF归一到 1m 的飞行距离;此时中子TOF与中子能量En存在一一对应关系,与入射位置无关。
c1->Clear();
c1->Divide(2,1);
c1->cd(1);
tree->Draw("tof_true:x_true >> h1(400, -1.10, 1.10, 400, 15, 60)", "", "colz");
c1->cd(2);
tree->Draw("tof_true/sqrt(pow(5.05,2)+pow(x_true,2)):x_true >> h2(400, -1.10, 1.10, 400, 2.5, 11", "", "colz");
c1->Draw();
4.6 位置¶
$$ x_{t} = \frac{v_{sc}}{2} (t_L - t_R - (t_{0L}-t_{0R}))$$ $$ x_q = \frac{\lambda}{2}\frac{g_R}{g_L}\ln \frac{A_L}{A_R} $$
c1->Clear();
c1->Divide(2,1);
c1->cd(1);
tree->Draw("x_true:tR-tL >> h3(200, -20, 50, 200, -1.1, 1.1)", "", "colz");
c1->cd(2);
tree->Draw("log(AL/AR):x_true","","colz");
c1->Draw();
5. 作业¶
修改并完善代码
目前的演示代码对物理过程进行了简化(例如假设中子能量单一、能量沉积固定等)。
根据下列要求,修改代码:
中子入射能量分布 ($E_n$):
- 中子源产生的能量并非单一值, 中子能量围绕中心值
En0的高斯展宽(EnFWHM)。
- 中子源产生的能量并非单一值, 中子能量围绕中心值
中子能量沉积:
- 模拟代码中 $A = A_{qe} * E_{dep}$。 根据讲义,在代码中加入探测器能量响应,即: $E_n\to E_p \to E_{ee}$, $Q = A_{qe} * E_{ee}$。
Time-walk:
- 新增 tL_true, tR_true 两个Branch, 这两个变量分别等于模拟代码中的tL和tR。
- 变量 tL 和 tR: 在tL_true, tR_true 变量上,加入时间分辨随幅度变化的项以及Time-Walk 项。
电子学阈值与触发逻辑 (Trigger):
- 实验中存在电子学阈值(Threshold)。只有当探测器两端信号幅度($A_L$ 和 $A_R$)各自都超过设定阈值时,该事件才会被记录。对应实验中的符合触发逻辑(Coincidence Trigger)。
6. 考虑作业中修正后的结果¶
.L create_tree_hw.cc
create_tree_hw();
Starting Simulation... Processing: 100% completed! Data saved to tree_hw.root
TFile *ipfm = new TFile("tree_hw.root");
c1->Clear();
c1->Divide(1,1);
6.1 Timewalk¶
- timewalk导致时间谱产生严重的畸变,下一步必须对其进行修正。
tree->Draw("tL-tL_true:AL>>(400,0,600,200,0,50)","","colz");
c1->Draw();
gamma事件选择¶
- Gamma以光速飞行,其飞行时间最短,因此对应图中下方那条高度集中且平滑的窄带。在进行 Time-Walk 校准前,可以通过设置Graphical Cut,提取这Gamma 事件(见文档附录)。
- 下列代码保存到draw_gamma_cut.cc
void draw_gamma_cut() {
TCutG *cutg = new TCutG("cut_gamma",15);
cutg->SetVarX("tL+tR");
cutg->SetVarY("sqrt(AL*AR)");
cutg->SetTitle("Graph");
cutg->SetFillStyle(1000);
cutg->SetPoint(0,86.7586,588.026);
cutg->SetPoint(1,87.9764,331.974);
cutg->SetPoint(2,89.803,105.395);
cutg->SetPoint(3,96.5007,7.76315);
cutg->SetPoint(4,118.116,-12.5);
cutg->SetPoint(5,131.816,-3.28948);
cutg->SetPoint(6,128.771,15.1316);
cutg->SetPoint(7,113.549,28.0263);
cutg->SetPoint(8,101.981,55.6579);
cutg->SetPoint(9,96.1963,156.974);
cutg->SetPoint(10,94.6741,304.342);
cutg->SetPoint(11,94.0652,503.289);
cutg->SetPoint(12,92.8474,591.711);
cutg->SetPoint(13,85.8453,586.184);
cutg->SetPoint(14,86.7586,588.026);
// 注意:在分析代码中调用时,通常不需要它自动 Draw()
cutg->Draw("L");
}
TCanvas *c2 = new TCanvas("tof-q","tof-q",1000,300);
c2->Divide(3,1);
c2->cd(1);
tree->Draw("sqrt(AL*AR):tL_true+tR_true>>h1(400,80,200,500,0,600)","","colz");
c2->cd(2);
tree->Draw("sqrt(AL*AR):tL+tR>>h2(400,80,250,500,0,600)","","colz");
draw_gamma_cut();
c2->cd(3);
tree->Draw("sqrt(AL*AR):tL+tR>>h3(400,80,250,500,0,600)","cut_gamma","colz");
c2->Draw();
附录:CERN ROOT Graphic Cut (TCutG) 指南¶
1. 建立上下文:绘制散点图¶
所有的图形化裁剪都始于对数据的可视化。
// 在终端执行,假设 x 为横坐标变量,y 为纵坐标变量
tree->Draw("y:x", "", "colz");
注意:在 Draw 命令中,顺序是 "纵轴:横轴"。
2. 交互式创建与重命名¶
- 激活工具:在 Canvas 菜单栏点击
View -> Toolbar。点击图标中的 "CutG"(✂️)。 - 绘制区域:在图中点击若干个点围成感兴趣的区域,双击最后一个点闭合。此时,该对象默认名为
CUTG。 - 修改名称:
- 将鼠标悬停在 Cut 边缘,右键点击 ->
SetName。 - 输入新名称,例如
mycut。这一步至关重要,因为后续调用都依赖此名称。
- 将鼠标悬停在 Cut 边缘,右键点击 ->
3. 保存与复用 (.C 脚本)¶
将你画好的形状保存下来,以便在以后的脚本或实验中使用:
操作:右键点击 Cut 边缘 ->
SaveAs->mycut.C。加载:在新的 ROOT 进程中,通过以下命令找回该 Cut:
root [0] .x mycut.C; // 执行后,名为 "mycut" 的对象已加载至内存
4. 在 ROOT 终端直接调用¶
加载 mycut 后,它可以直接作为 TTree::Draw 的过滤条件,并支持与普通字符串条件混用。
基本应用:
tree->Draw("y:x", "mycut"); // 仅显示 mycut 区域内的数据
条件混用:
// 逻辑组合:在 mycut 区域内,且满足 x > 100 且 y > 50 tree->Draw("y:x", "mycut && x > 100 && y > 50");
5. 在循环代码中引用 (C++ Loop)¶
如果你在编写 for 循环手动处理每个 Event,需要使用 IsInside 方法。
⚠️ 核心要点:参数顺序必须与定义一致
在第一步 tree->Draw("y:x") 中,你的横轴是 x,纵轴是 y。因此在调用 IsInside 时,必须严格保持 (横轴变量, 纵轴变量) 的顺序。
// 1. 加载 Cut 对象
gROOT->ProcessLine(".x mycut.C");
TCutG *mycut = (TCutG*)gROOT->FindObject("mycut");
// 2. 进入事件循环
for (Long64_t i = 0; i < tree->GetEntries(); ++i) {
tree->GetEntry(i);
// 判定:第一个参数是绘图时的横轴(x),第二个参数是纵轴(y)
// 变量顺序如果写反为 (valY, valX),判定结果将完全错误
if (mycut->IsInside(valX, valY)) {
// 逻辑处理...
}
}