Skip to content

\(\Lambda\)粒子

在这一章我们一起来重建\(\Lambda\),那在开始之前,你需要在PDG上仔细地研究一下这个粒子。它的质量大概是 \(1.115GeV\),而寿命 \(\tau_{\Lambda}\approx2.617\times10^{−10}s\),比 \(K_s\)的寿命大了3倍,我们同样可以利用这一显著的特征来去除本底。再看它的衰变模式,我们发现\(\Lambda\rightarrow p^+\pi^-\)的分支比大概是\(64.1\%\),因此我们可以在 \(p^+\pi^-\)衰变道来寻找\(\Lambda\)粒子。

1-Ntuple Maker

\(p^+\pi^-\)的寻找和\(\pi^+\pi^-\)一样,都是通过带电粒子的track来匹配和重建的。两者唯一的区别在于two tracks fit部分。

对于\(\pi^+\pi^-\),由于两者质量相同,无论我们认为第一个 \(\pi\) 的track是track1还是track2其实都一样。

1
2
3
4
//构建一个RefCountedKinematicParticle类型的vector,把我们想要拟合的粒子都放进去
vector < RefCountedKinematicParticle > PiPiParticles;
PiPiParticles.push_back(PiPiFactory.particle(trackTT1, pion_mass, chi, ndf, pion_sigma));
PiPiParticles.push_back(PiPiFactory.particle(trackTT2, pion_mass, chi, ndf, pion_sigma));

但是对于\(p^+\pi^-\)\(p^+\)的质量是 \(0.938GeV\)\(\pi^-\)的质量是 \(0.139GeV\),两者相差了9倍,在物理上我们可以证明:在实验室系中,\(p^+\)的动量大于 \(\pi^-\)

这里给出证明。

\(\Lambda\)质心系,\(\Lambda\)的动量为0,根据动量守恒定律,\(|\vec{p_p^*}|=|\vec{p_\pi^*}|=p^*\),能量 \(E_p^*=\sqrt{p{^*}^2+m_p^2}\)\(E_\pi^*=\sqrt{p{^*}^2+m_\pi^2}\),由于 \(m_p > m_\pi\),所以 \(E_p^* > E_\pi^*\)

\(p\)的动量在质心系与z轴夹角为 \(\theta\),则\(p\)的纵向分量 \(p_{p,z}^* = p^*\cos\theta\)\(\pi\)的纵向分量 \(p_{\pi,z}^* = -p^*\cos\theta\)。两者横向分量相等,\(p_T^* = p^*\sin\theta\)

\(\Lambda\)做沿着+z方向的boost,由洛伦兹变换

\[\begin{pmatrix} E \\ p_x \\ p_y \\ p_z \end{pmatrix} = \begin{pmatrix} \gamma & 0 & 0 & \gamma\beta \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ \gamma\beta & 0 & 0 & \gamma \end{pmatrix} \begin{pmatrix} E^* \\ p_x^* \\ p_y^* \\ p_z^* \end{pmatrix}\]

可得 \(p_{p,z}=\gamma(\beta E_p^*+p_{p,z}^*)=\gamma(\beta E_p^*+p^*\cos\theta)\)\(p_{\pi,z}=\gamma(\beta E_\pi^*+p_{\pi,z}^*)=\gamma(\beta E_\pi^*-p^*\cos\theta)\),而横向分量在boost下不变,\(p_T=p_T^*=p^*\sin\theta\)

为了简便,我们假设 \(\theta=0\)

则差值 \(\Delta p^2 = p_p^2-p_\pi^2 = p_{p,z}^2-p_{\pi,z}^2=\gamma^2(E_p^*+E_\pi^2)[2\beta p^*+\beta^2(E_p^*-E_\pi^*)]>0\)。得证。

因此我们只需要修改two tracks fit部分的代码。

//MuMuEEPat.cc
void MuMuEEPat::analyze(const edm::Event& iEvent, const edm::EventSetup& iSetup)
{
  const double myPimass=0.13957039;
  const double myPimasserr = myPimass * 1e-6;
  const double myProtonmass=0.93827208816;
  const double myProtonmasserr = myProtonmass * 1e-6;
  const MagneticField &bFieldHandle = iSetup.getData(magneticFieldToken_);//在获取track信息时需要磁场信息
  ......
  if ( (iTrack1->charge()+iTrack2->charge() ) == 0 )
  {
    TransientTrack trackTT1(*iTrack1->bestTrack(), &(bFieldHandle));
        TransientTrack trackTT2(*iTrack2->bestTrack(), &(bFieldHandle));
        KinematicParticleFactoryFromTransientTrack ProPiFactory;
    // pdg mass
    ParticleMass pion_mass = myPimass;
    float pion_sigma = myPimasserr;
    ParticleMass proton_mass = myProtonmass;
    float proton_sigma = myProtonmasserr;

    // initial chi2 and ndf before kinematic fits.
    float chi = 0.;
    float ndf = 0.;
    vector < RefCountedKinematicParticle > ProPiParticles;
    //-----------bigger momentum is proton------------//
    bool Track1IsProton= true;//用来识别proton的track,方便我们后续存储对应的四动量
    //这里我们并没有对Track的电荷做限制,因为lambda-> p+ + pi-,lambdabar-> p- + pi+
    if (((iTrack1->momentum().x()*iTrack1->momentum().x() + iTrack1->momentum().y()*iTrack1->momentum().y() + iTrack1->momentum().z()*iTrack1->momentum().z())> (iTrack2->momentum().x()*iTrack2->momentum().x() + iTrack2->momentum().y()*iTrack2->momentum().y() + iTrack2->momentum().z()*iTrack2->momentum().z())))
    {
      ProPiParticles.push_back(ProPiFactory.particle(trackTT1, proton_mass, chi, ndf, proton_sigma));
      ProPiParticles.push_back(ProPiFactory.particle(trackTT2, pion_mass, chi, ndf, pion_sigma));
    }
    else if (((iTrack1->momentum().x()*iTrack1->momentum().x() + iTrack1->momentum().y()*iTrack1->momentum().y() + iTrack1->momentum().z()*iTrack1->momentum().z())> (iTrack2->momentum().x()*iTrack2->momentum().x() + iTrack2->momentum().y()*iTrack2->momentum().y() + iTrack2->momentum().z()*iTrack2->momentum().z())))
    {
      Track1IsProton= false;
      ProPiParticles.push_back(ProPiFactory.particle(trackTT1, pion_mass, chi, ndf, pion_sigma));
      ProPiParticles.push_back(ProPiFactory.particle(trackTT2, proton_mass, chi, ndf, proton_sigma));
    }
    else{continue;}
    //-----------bigger momentum is proton------------//
    KinematicParticleVertexFitter fitter;
    RefCountedKinematicTree ProPiVertexFitTree;
    ProPiVertexFitTree = fitter.fit(ProPiParticles);
    if (ProPiVertexFitTree->isValid())
    {
      ProPiVertexFitTree->movePointerToTheTop();
      RefCountedKinematicParticle ProPi_vFit_noMC = ProPiVertexFitTree->currentParticle();
      RefCountedKinematicVertex ProPi_vFit_vertex_noMC = ProPiVertexFitTree->currentDecayVertex();
      KinematicParameters mypropipara=  ProPi_vFit_noMC->currentState().kinematicParameters();

      if (ProPi_vFit_vertex_noMC->vertexIsValid())
      {
        //仿照Ks把变量存储下来吧
      }
    }//顶点拟合有效
  }//if if two track charge is 0
}

2-产生multilep.root文件

这一部分和 \(K_s\)类似,我们要交crab作业来产生大量multilep.root

3-myntuple分析root文件

这一部分和 \(K_s\)类似,下面仅列出我的myntuple.C的筛选条件部分作为参考。

1
2
3
4
5
//这里仅对pion的pt做了cut,因为proton的pt一定是比pion高的。如果仍然把pion的pt设置为大于1.2,这会cut掉非常多的事例
if ( (*propionlyctau)[myi] > 2 && (*propionlyVtxCL)[myi] > 0.05 && (*pion2Pt)[myi] > 0.5) 
{
  propimass->Fill((*propionlyMass)[myi]);
}

这里要说明一下,我当时在.cc中限制了\(p\)为正电荷,\(\pi\)为负电荷,因此我下面重建的结果都是\(\Lambda\)正粒子。

最终得到的\(\Lambda\)粒子长成这样

截屏2025-11-14 19.48.04

看到这个峰是不是有点激动?你会感觉\(\Lambda\)粒子比 \(K_s\)更容易找到,主要是有以下两点原因:

  1. 在two tracks fit中,将大动量的track赋给了proton,小动量的track赋给了pion,相当于手动排除了另一种可能性,大大减少了错误的重建(本底)。
  2. \(p + \pi\)的质量阈值大概在 \(1.07GeV\),而\(\Lambda\)粒子的质量大概是 \(1.115GeV\),略高于阈值,而本底在阈值附近会因为相空间受限而迅速减少,因此你看到的\(\Lambda\)\(K_s\)相比非常干净。

4-Fit

找到 \(\Lambda\) 粒子之后你同样需要对它做拟合。由于 \(\Lambda\) 的宽度很窄,所以这里采用高斯函数+水晶球函数拟合信号,二阶切比雪夫多项式拟合本底。下面仅列出我的fitLamda.C作为参考。

#include "RooAbsReal.h"
#include "RooRealVar.h"
#include "RooGaussian.h"
#include "RooChebychev.h"
#include "RooAddPdf.h"
#include "RooDataSet.h"
#include "RooPlot.h"
using namespace RooFit;

void fitLamda() 
{ 
  gROOT->SetBatch(kTRUE);//一个小trick
  int STRATEGY = 1; //0, 1, 2
  int NCPU = 4; //num of cpus
  int FFT_BINS = 10000;
  bool MINOS = false;

  double mxMin = 1.08, mxMax = 1.15;
  int mxBins = 200;
  double mxBinWidth = (mxMax - mxMin) / mxBins;

  TString XTitle, YTitle;
  XTitle.Form("m(p^{+}#pi^{-}) (GeV)");
  YTitle.Form("Candidates / %.2f MeV", mxBinWidth * 1000);

  ofstream fitresult("fitoutput/fitLamdaResult.txt");

  RooRealVar mx("mx", "mx", mxMin, mxMax);
  RooArgSet variables;
  variables.add(mx);

  RooDataSet data_sig = *RooDataSet::read("outLamda1stNovselect.txt",variables, "Q");
  mx.setBins(mxBins);
  RooDataHist datahist("datahist","binned version of dataset",RooArgSet(mx),data_sig);//Binned fit

  RooRealVar mean("mean", "mean", 1.115, 1.11, 1.12);
  RooRealVar sigmagaus("sigmagaus", "sigmagaus", 0.006, 0, 0.01);
  RooRealVar sigmaCB("sigmaCB", "sigmaCB", 0.006, 0, 0.01);
  RooRealVar alpha("alpha", "alpha", -6, -10, -0.5);
  RooRealVar n("n", "n", 5, 0, 50);
  RooAbsPdf* gaus = new RooGaussian("gaus", "gaus", mx, mean, sigmagaus);
  RooAbsPdf* CB = new RooCBShape("CB", "CB", mx, mean, sigmaCB, alpha, n);
  RooRealVar frac("frac", "frac", 0.5, 0, 1);
  RooAddPdf* gausCB= new RooAddPdf("gausCB", "gausCB",RooArgList(*gaus,*CB),frac);
  RooRealVar nsig("nsig","nsig",0,0,50000000);
  RooExtendPdf* sig=new RooExtendPdf("sig","sig",*gausCB,nsig);

  RooRealVar a0("a0","a0",0.005,-10,10);
  RooRealVar a1("a1","a1",0.005,-10,10);
  RooRealVar a2("a2","a2",0.005,-10,10);
  RooAbsPdf* shev=new RooChebychev("shev","shev",mx,RooArgList(a0,a1,a2));
  RooRealVar nbkg("nbkg","nbkg",0,0,50000000);
  RooExtendPdf* bkg=new RooExtendPdf("bkg","bkg",*shev,nbkg);

  RooAddPdf null_Pdf("null_Pdf","null_Pdf",RooArgList(*bkg),RooArgList(nbkg));
  RooAddPdf sig_Pdf("sig_Pdf","sig_Pdf",RooArgList(*sig,*bkg),RooArgList(nsig,nbkg));

  double significance;
  //fit
  for(int k=0;k<20; k++) 
  {
    RooFitResult *null_result= null_Pdf.fitTo(datahist, Save(kTRUE), Minos(MINOS), Strategy(STRATEGY), NumCPU(NCPU));
    double edm_null = null_result->edm();
    double minNll_null = null_result->minNll(); //Return minimized -log(L) value.
    int status_null = null_result->status();
    int covQual_null = null_result->covQual();
    if(edm_null<0.001 && status_null==0 && covQual_null==3)
    {
      RooFitResult *sig_result= sig_Pdf.fitTo(datahist, Save(kTRUE), Minos(MINOS), Strategy(STRATEGY), NumCPU(NCPU));
      double edm_sig = sig_result->edm();
      double minNll_sig = sig_result->minNll(); //Return minimized -log(L) value.
      int status_sig = sig_result->status();
      int covQual_sig = sig_result->covQual();
      if(edm_sig<0.001 && status_sig==0 && covQual_sig==3)
      {
        significance = sqrt(2*fabs(minNll_null-minNll_sig));

        RooPlot *xframe = mx.frame(RooFit::Title("Fit #Lambda"), Range(mxMin, mxMax), Bins(mxBins));
        datahist.plotOn(xframe, Name("data"));
        sig_Pdf.plotOn(xframe, Name("model"), LineColor(kBlue), LineStyle(1));
        sig_Pdf.plotOn(xframe, Components(*sig),LineStyle(kDashed),LineColor(2), Name("signal"));
        sig_Pdf.plotOn(xframe, Components(*bkg),LineStyle(kDashed),LineColor(6), Name("background"));

        //=======write fit parameters ======//
        RooArgSet params(mean, sigmagaus, sigmaCB, alpha, n, frac);
        RooPlot* paramBox = sig_Pdf.paramOn(xframe, Parameters(params), Layout(0.12, 0.46, 0.9), Format("NEU", AutoPrecision(2)));
        TPaveText *pt = new TPaveText(0.12, 0.4, 0.4, 0.5, "NDC");
        pt->SetFillColor(0);
        pt->SetTextColor(kRed);
        pt->SetTextSize(0.045);
        pt->SetTextFont(42);
        pt->AddText(Form("Width = %.2f MeV", widthFWHM));
        pt->AddText(Form("Significance = %.2f #sigma", significance));
        //=======write fit parameters ======//

        xframe->GetXaxis()->SetTitle(XTitle.Data());
        xframe->GetXaxis()->SetLabelColor(0, 0);
        xframe->GetYaxis()->SetTitle(YTitle.Data());
        xframe->GetYaxis()->SetTitleSize(0.05);
        xframe->GetYaxis()->SetLabelSize(0.05);
        xframe->GetYaxis()->SetTitleOffset(0.8);

        TLegend leg1(0.7, 0.7, 0.9, 0.9);
        leg1.AddEntry(xframe->findObject("data"), "Data", "pe");
        leg1.AddEntry(xframe->findObject("model"), "Fit", "l");
        leg1.AddEntry(xframe->findObject("signal"), "Signal", "l");
        leg1.AddEntry(xframe->findObject("background"), "Bkg", "l");

        RooPlot *pullFrame1 = mx.frame(RooFit::Title(" "), Range(mxMin, mxMax), Bins(mxBins));
        RooHist *pull1 = xframe->pullHist("data", "model");
        pullFrame1->addPlotable(pull1, "p");
        pullFrame1->GetXaxis()->SetTitle(XTitle.Data());
        pullFrame1->GetXaxis()->SetTitleSize(0.15);
        pullFrame1->GetXaxis()->SetLabelSize(0.12);
        pullFrame1->GetYaxis()->SetTitle("Pull");
        pullFrame1->GetYaxis()->SetTitleSize(0.15);
        pullFrame1->GetYaxis()->SetLabelSize(0.1);
        pullFrame1->GetYaxis()->SetTitleOffset(0.2);

        TCanvas c1("c1", "c1", 800, 600);
        c1.cd();
        TPad pad11("pad11", "pad11", 0, 0.3, 1, 1.0);
        pad11.SetTopMargin(0.08);
        pad11.SetBottomMargin(0.017);
        pad11.Draw();
        pad11.cd();
        xframe->Draw();
        leg1.Draw();
        pt->Draw("same");
        c1.cd();
        TPad pad12("pad12", "pad12", 0.0, 0.0, 1, 0.3);
        pad12.SetTopMargin(0.03);
        pad12.SetBottomMargin(0.325);
        pad12.SetGridx();
        pad12.SetGridy(2);
        pad12.Draw();
        pad12.cd();
        pullFrame1->Draw();
        c1.Update();
        c1.SaveAs("fitoutput/figure/fitLamda_sig.pdf");

        break;
      } 
    } 
  }//for(int k=0;k<20; k++) 
  fitresult << std::fixed
            << "NumSig = " << nsig.getVal() << " ± "<< nsig.getError() << " "
            << "Mean  = " << mean.getVal() << " ± " << mean.getError() << " "
            << "Significance = " << significance << " "
            << std::endl;

  std::cout << "Mean  = " << mean.getVal() << " ± " << mean.getError() << std::endl;
  std::cout << "NumSig = " << nsig.getVal() << " ± " << nsig.getError() << std::endl;
  std::cout << "Significance = " << significance << std::endl;
  fitresult.close();
}  

这里有几点小trick:

  1. 你会发现上面的直方图的事例非常多,我们需要考虑是采用Unbinned fit还是Binned fit。Unbinned fit是对每一个数据点进行拟合,Binned fit是对每个bin进行拟合,他们之间有几个数量级的差别。如果\(\Lambda\)\(K_s\)一样采用Unbinned fit,那拟合的速度会非常慢,效率极低因此这里选择做Binned fit。
  2. 细心的你会发现\(\Lambda\)的拟合程序有这么一句话:gROOT->SetBatch(kTRUE);,这个命令是要求程序拟合完之后不产生图直接结束运行的,图会保存在相应的pdf里。像lxplus和lpc这样的国外远程服务器,产生图的速度会非常慢,效率极低。我们可以直接把pdf拷贝到本地来查看,速度翻番!

拟合结果已经展示在图上。注意这里仅重建了\(\Lambda\)正粒子。同样,在这里看到的信号“宽度”其实并不是\(\Lambda\)真正的宽度,它是由探测器的resolution(分辨率)所决定的。

截屏2025-11-14 20.40.42

恭喜你又成功地完整地打通了\(\Lambda\)这一关!凭借自己的努力找到粒子是不是非常有成就感?将抽象的粒子具象化是不是感觉还挺有趣的?但是故事还没有结束......下一章我们会继续挑战重建\(\Omega^-\)粒子!