How do I use DecayTreeFitter?

Learning Objectives

  • Add a kinematic fitter to a branch in the decay tree

  • Apply a mass constraint

  • Inspect the refitted decay tree

Once you have made a hypothesis on the chain of decays that lead to your final state, you then can incorporate the additional knowledge that comes with this hypothesis to get a new best estimate for the particle parameters – in particular their momenta. The additional knowledge is represented as constraints, which your decay tree has to fulfill.

For example, for the decay

'[D*(2010)+ -> (D0 -> K- K+) pi+]CC'

you can make the assumption that the (K- K+) combine to form a D0 with a specific invariant mass. This results in a so called mass constraint. In addition the two kaons should originate from exactly the same point in space. If you know that your data only contains prompt D* candidates, you can constrain them to do come from the primary vertex. Boundary conditions like those are called vertex constraints (the last of which is known as a primary-vertex constraint).

Applying kinematic constraints leads to new best estimates for the track parameters of the final-state particles. The process of calculating those is called a kinematic (re)fit and the DecayTreeFitter is the algorithm that performs this task for us. Access to this tool is provided by the TupleTool with the name TupleToolDecayTreeFitter.

The physics and mathematics behind DecayTreeFitter

For details of the method see the paper on Decay chain fitting with a Kalman filter.

So how do we use a TupleToolDecayTreeFitter in our DaVinci script? Let’s create a branch to add the tool to. We’ll just name it 'Dstar':

dtt.addBranches({
    'Dstar': '[D*(2010)+ -> (D0 -> K- K+) pi+]CC',
})

To this branch we can now apply the TupleToolDecayTreeFitter with arbitrarily chosen name consD.

dtt.Dstar.addTupleTool('TupleToolDecayTreeFitter/ConsD')

Now we can proceed with the configuration of the fitter. We are going to constrain the D0 to have originated from the primary vertex. We want all the output available, so we set the verbose option. Finally we want to apply the mass constraint to the D0:

dtt.Dstar.ConsD.constrainToOriginVertex = True
dtt.Dstar.ConsD.Verbose = True
dtt.Dstar.ConsD.daughtersToConstrain = ['D0']

Note that you can constrain more than one intermediate state at once if that fits your decay.

When using the TupleToolDecayTreeFitter in a DecayTreeTuple, all the variables created by the other TupleTools are not affected by the change, but some new variables are created, one set per DecayTreeFitter instance. Depending on whether the Verbose option is specified, the new variables are created for the head particle only or for the head particle and its decay products too.

If the decay products are not stable particles and decay further, the decay products of the decay products have no new variables associated to them by default. Since in many cases this information might be useful, there is an option to store the information from those tracks

dtt.Dstar.ConsD.UpdateDaughters = True

DecayTreeFitter and LoKi functors

Alternatively, many of the operations described above can be done by using the DecayTreeFitter via LoKi functors using DTF_FUN functor in LoKi::Hybrid::TupleTool. The advantage of this method is that it allows to customize the output variables as shown below.

LoKi_DTFFun = dtt.Dstar.addTupleTool("LoKi::Hybrid::TupleTool/LoKi_DTFFun")
LoKi_DTFFun.Variables = {
    "DTFFun_Dstart_P"   : "DTF_FUN(P, True, 'D0')",
    "DTFFun_Dstar_PT"   : "DTF_FUN(PT, True, 'D0')",
    "DTFFun_Dstar_M"    : "DTF_FUN(M, True, 'D0')",
    "DTFFun_DTF_CHI2"   : "DTF_CHI2(True, 'D0')",
    "DTFFun_DTF_NDOF"   : "DTF_NDOF(True, 'D0')",
    "DTFFun_D0_M"       : "DTF_FUN(CHILD(M,  '[D*(2010)+ ->^D0 pi+]CC'), True, 'D0')",
    "DTFFun_D0_PE"      : "DTF_FUN(CHILD(E,  '[D*(2010)+ ->^D0 pi+]CC'), True, 'D0')",
    "DTFFun_D0_PX"      : "DTF_FUN(CHILD(PX, '[D*(2010)+ ->^D0 pi+]CC'), True, 'D0')",
    "DTFFun_D0_PY"      : "DTF_FUN(CHILD(PY, '[D*(2010)+ ->^D0 pi+]CC'), True, 'D0')",
    "DTFFun_D0_PZ"      : "DTF_FUN(CHILD(PZ, '[D*(2010)+ ->^D0 pi+]CC'), True, 'D0')"
}

The first argument of DTF_FUN is the LoKi functor that defines the output variable. The second (boolean) argument defines if primary-vertex constraint is required or not. The third argument is optional and specifies a particle or list of particles to be mass-constrained. In the case of multiple mass constraints this argument should look like strings(['particle1','particle2']). The quality of the fit can be accessed by DTF_CHI2 functor.

The DecayTreeFitter implementation described above has a disadvantage that it will re-run the fit for every variable requested by DTF_FUN. A more efficient and strongly recommended way to use LoKi-based DecayTreeFitter can be done using LoKi__Hybrid__Dict2Tuple tool as described in DaVinci tutorial.

Which constraints to apply

It is important to be aware of the assumptions you make to build your ntuple. For example, after you require the vertex constraint you must be careful if using the IPCHI2_OWNPV, since the particle you are looking at is forced to point to the PV. Which constraints make most sense for you depends on the questions you want to ask in your analysis, so ask your supervisor/working group in case of doubt.

Once you have produced your ntuple you can have a look at the refitted variables.

root -l DVntuple.root
TupleDstToD0pi_D0ToKK->cd()
DecayTree->StartViewer()

Plotting the raw mass of the D* (without the fit) Dstar_M, you should see a broad signal around 2 GeV:

Dstar raw

Which mass variable to use

In many ntuples you also find a mass variable called _MM. This (confusingly) refers to measured mass. However, it is usually better to use _M. _MM is the sum of the 4-momenta of the final state particles extrapolated back to the fitted vertex position, but not the result of the actual vertex fit. Remember that a vertex fit acts like a vertex constraint, improving the opening-angle resolution.

Now let us look at the refitted mass of the \(` D^{*+} `\), with the \(` D^0 `\) constrained to its nominal mass. It is stored in the variable Dstar_ConsD_M. If you plot this you will note that some values are unphysical. So, let’s restrict the range we look at to something that makes sense.

On the root prompt use the arrow-up key to get the last draw command and modify it to pipe the output into a histogram:

tv__tree->Draw("Dstar_ConsD_M>>h(200,2000,2030)","","");

Dstar refitted

Note that this plot has 149 entries, although we only have 73 candidates in the raw mass spectrum. The reason for this is that we typically have several primary vertices per event. When you use the vertex constraint, the fitter is run for each of the possible vertex hypotheses available in the event. So all the Dstar_ConsD-xxx variables are in fact arrays, where the first value corresponds to the best PV hypothesis. We can plot only those by doing

tv__tree->Draw("Dstar_ConsD_M[0]>>h(200,2000,2030)","","");

and we get the final kinematically refitted Dstar mass:

Dstar refitted best PV

Finally, let’s check how the D0 mass constraint has played out.

tv__tree->Draw("Dstar_ConsD_D0_M[0]>>h(100,1800,1900)","","", 128, 0);

constrained D0

As expected, the D0 candidates are forced onto their PDG mass value.

Explore

  • Look at the status variable to check if the fits converged.

  • Look at the chi2 distribution of the fit

DecayTreeFitter can be told to change some of the hypotheses in the decay tree. This is very useful if you want to slightly change which decays you want to look at. As an example let’s say we want to examine the decay of the D0 into K- pi+ instead of K- K+. For this we add a second fitter, giving it a new name ConsDKpi:

dtt.Dstar.addTupleTool('TupleToolDecayTreeFitter/ConsDKpi')
dtt.Dstar.ConsDKpi.constrainToOriginVertex = True
dtt.Dstar.ConsDKpi.Verbose = True
dtt.Dstar.ConsDKpi.daughtersToConstrain = ['D0']

We now can tell the fitter to substitute one of the kaons in the D0 decay by a pion.

dtt.Dstar.ConsDKpi.Substitutions = {
    'Charm -> (D0 -> K- ^K+) Meson': 'pi+',
    'Charm -> (D~0 -> K+ ^K-) Meson': 'pi-'
}

In the dictionary that is passed to the Substitutions property of the fitter, the keys are decay descriptors, where the respective particle to be substituted is marked with a ^. The values are the respective new particle hypotheses. The substitution will only work if you start from a decay descriptor that actually matches your candidates. However, you are allowed to generalise parts of the decay. Here we replaced D*(2010) with the more general Charm and the bachelor pi- is just represented by a Meson.

Note that the substitution mechanism does not understand the CC symbol. Both charge states have to be specified explicitely.

Running the ntuple script again with these additions gives you fit results for the re-interpreted decay.

Challenge

  • Compare the outcome of the two fits with the different mass hypothesis

  • Compare the fit quality between the correct and the the wrong hypothesis

The solution to this exercise ntuple_DTF1.py, is available here.