# 21.4.5. Flavor tagging¶

How can we distinguish between a $$B^0$$ and a $$\bar B^0$$? This is not as simple as the case where we have a $$B^+$$ vs a $$B^-$$ and can just consider the charge of the particles. Yet determining this “flavor” of the $$B$$ meson is crucial to look into CP violation.

This is where the Flavor Tagger comes in. Used in an analysis, where we have reconstructed a signal $$B_\text{sig}$$, the Flavor Tagger looks at the ROE of the $$B_\text{sig}$$ (i.e. at the $$B_\text{tag}$$) and applies machine learning techniques (also called multivariate analysis) to determine the flavor of the $$B_\text{tag}$$. In this lesson we will not train this model ourselves, but rather use pre-computed weights. So where do we get them?

Luckily, such sets of weights are contained in the conditions database. That means that we only need to use a specific global tag (if you forgot what that is, consult this page again: Conditions Database Overview).

Exercise

Add a line to your previous steering file that adds the analysis_tools_release-04-02 global tag. For this you need to call the prepend_globaltag method of the basf2.conditions object (which is an instance of the ConditionsConfiguration class).

Solution

 15 16 # set analysis global tag (needed for flavor tagging) b2.conditions.prepend_globaltag("analysis_tools_release-04-02") 

Good! Now we have the weights and we have already created the ROE, so we can start right away!

Exercise

Call the flavorTagger.flavorTagger function (you need to specify your path and your particle list).

Solution

Remember to import the module:

 7 import flavorTagger as ft 

and call the function:

 77 78 # call flavor tagging ft.flavorTagger(["B0"], path=main) 

Exercise

Add the the flavorTagger.flavor_tagging variable collection to your output variables

Solution

 105 b_vars += ft.flavor_tagging 

Exercise

Solution

This is the full steering file at this point:

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 #!/usr/bin/env python3 import sys import basf2 as b2 import modularAnalysis as ma import stdV0s import flavorTagger as ft from variables import variables as vm # shorthand for VariableManager import variables.collections as vc import variables.utils as vu # get input file number from the command line filenumber = sys.argv[1] # set analysis global tag (needed for flavor tagging) b2.conditions.prepend_globaltag("analysis_tools_release-04-02") # create path main = b2.Path() # load input data from mdst/udst file ma.inputMdstList( environmentType="default", filelist=[b2.find_file(f"starterkit/2021/1111540100_eph3_BGx0_{filenumber}.root", "examples")], path=main, ) # fill final state particle lists ma.fillParticleList( "e+:uncorrected", "electronID > 0.1 and dr < 0.5 and abs(dz) < 2 and thetaInCDCAcceptance", path=main, ) stdV0s.stdKshorts(path=main) # apply Bremsstrahlung correction to electrons vm.addAlias( "goodFWDGamma", "passesCut(clusterReg == 1 and clusterE > 0.075)" ) vm.addAlias( "goodBRLGamma", "passesCut(clusterReg == 2 and clusterE > 0.05)" ) vm.addAlias( "goodBWDGamma", "passesCut(clusterReg == 3 and clusterE > 0.1)" ) vm.addAlias( "goodGamma", "passesCut(goodFWDGamma or goodBRLGamma or goodBWDGamma)" ) ma.fillParticleList("gamma:brems", "goodGamma", path=main) ma.correctBrems("e+:corrected", "e+:uncorrected", "gamma:brems", path=main) vm.addAlias("isBremsCorrected", "extraInfo(bremsCorrected)") # combine final state particles to form composite particles ma.reconstructDecay( "J/psi:ee -> e+:corrected e-:corrected ?addbrems", cut="dM < 0.11", path=main, ) # combine J/psi and KS candidates to form B0 candidates ma.reconstructDecay( "B0 -> J/psi:ee K_S0:merged", cut="Mbc > 5.2 and abs(deltaE) < 0.15", path=main, ) # match reconstructed with MC particles ma.matchMCTruth("B0", path=main) # build the rest of the event ma.buildRestOfEvent("B0", fillWithMostLikely=True, path=main) track_based_cuts = "thetaInCDCAcceptance and pt > 0.075 and dr < 5 and abs(dz) < 10" ecl_based_cuts = "thetaInCDCAcceptance and E > 0.05" roe_mask = ("my_mask", track_based_cuts, ecl_based_cuts) ma.appendROEMasks("B0", [roe_mask], path=main) # call flavor tagging ft.flavorTagger(["B0"], path=main) # perform best candidate selection b2.set_random_seed("Belle II StarterKit") ma.rankByHighest("B0", variable="random", numBest=1, path=main) # Create list of variables to save into the output file b_vars = [] standard_vars = vc.kinematics + vc.mc_kinematics + vc.mc_truth b_vars += vc.deltae_mbc b_vars += standard_vars # ROE variables roe_kinematics = ["roeE()", "roeM()", "roeP()", "roeMbc()", "roeDeltae()"] roe_multiplicities = [ "nROE_Charged()", "nROE_Photons()", "nROE_NeutralHadrons()", ] b_vars += roe_kinematics + roe_multiplicities # Let's also add a version of the ROE variables that includes the mask: for roe_variable in roe_kinematics + roe_multiplicities: # e.g. instead of 'roeE()' (no mask) we want 'roeE(my_mask)' roe_variable_with_mask = roe_variable.replace("()", "(my_mask)") b_vars.append(roe_variable_with_mask) b_vars += ft.flavor_tagging # Variables for final states (electrons, positrons, pions) fs_vars = vc.pid + vc.track + vc.track_hits + standard_vars b_vars += vu.create_aliases_for_selected( fs_vars + ["isBremsCorrected"], "B0 -> [J/psi -> ^e+ ^e-] K_S0", prefix=["ep", "em"], ) b_vars += vu.create_aliases_for_selected( fs_vars, "B0 -> J/psi [K_S0 -> ^pi+ ^pi-]", prefix=["pip", "pim"] ) # Variables for J/Psi, KS jpsi_ks_vars = vc.inv_mass + standard_vars b_vars += vu.create_aliases_for_selected(jpsi_ks_vars, "B0 -> ^J/psi ^K_S0") # Add the J/Psi mass calculated with uncorrected electrons: vm.addAlias( "Jpsi_M_uncorrected", "daughter(0, daughterCombination(M,0:0,1:0))" ) b_vars += ["Jpsi_M_uncorrected"] # Also add kinematic variables boosted to the center of mass frame (CMS) # for all particles cmskinematics = vu.create_aliases( vc.kinematics, "useCMSFrame({variable})", "CMS" ) b_vars += vu.create_aliases_for_selected( cmskinematics, "^B0 -> [^J/psi -> ^e+ ^e-] [^K_S0 -> ^pi+ ^pi-]" ) vm.addAlias( "withBremsCorrection", "passesCut(passesCut(ep_isBremsCorrected == 1) or passesCut(em_isBremsCorrected == 1))", ) b_vars += ["withBremsCorrection"] # Save variables to an output file (ntuple) ma.variablesToNtuple( "B0", variables=b_vars, filename="Bd2JpsiKS.root", treename="tree", path=main, ) # Start the event loop (actually start processing things) b2.process(main) # print out the summary print(b2.statistics) 

Good! Now let’s talk about the output of the flavor tagger. This is the value $$q\cdot r$$, where $$q=-1$$ corresponds to a $$\bar B^0$$ and $$q=+1$$ to $$B^0$$. $$r$$ is called the dilution factor. It’s 0 if the algorighm can’t decide between both options for $$q$$ and 1 if the algorithm is certain about it’s decision.

The variable FBDT_qrCombined is the $$q\cdot r$$ result of one of the models of the Flavor Tagger (a fast boosted decision tree). It can also be NaN to signal that not a single charged track in the ROE was found, so that the algorithm can’t work.

Note

In releases before release-05, a value of $$\pm 2$$ was used instead of NaN.

Part of the variables you just added was also qrMC for the “true” (MC level) flavor of the $$B_\text{tag}$$. It can take the numbers $$\pm 1$$, as well as 0 (no flavor defined in the MC) and $$\pm 2$$ (some problems with MC matching in the ROE).

That means that we can check how well our flavor tagger performed by comparing it to FBDT_qrCombined!

Exercise (optional)

Plot a histogram of FBDT_qrCombined and qrMC.

Exercise

Only consider candidates with clearly defined MC level flavor tag. Compare the output of qrMC to that of FBDT_qrCombined.

Hint

For clearly defined MC flavor tag, you only have to distinguish between qrMC == 0 and qrMC == 1. For these two cases you can then plot the distribution of FBDT_qrCombined.

Solution

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import matplotlib.pyplot as plt import root_pandas plt.style.use("belle2") df = root_pandas.read_root("Bd2JpsiKS.root") fig, ax = plt.subplots() ax.hist( df.query("qrMC == -1.")["FBDT_qrCombined"], histtype="step", linewidth=1.5, label=r"True $\bar B^0$", bins=30, ) ax.hist( df.query("qrMC == 1.")["FBDT_qrCombined"], histtype="step", linewidth=1.5, label=r"True $B^0$", bins=30, ) ax.set_xlabel("FBDT_qrCombined") ax.legend() fig.savefig("flavor_tags.svg") 

Fig. 21.24 MC flavor tag vs flavor tag from FBDT

You can clearly see that the flavor tagger is by no means perfect, but definitely allows to do better than just guessing!

Key points

• The flavor tagger is used to discriminate between $$B_\text{tag}^0$$ and $$\bar B_\text{tag}^0$$

• The output is of the form $$\pm 1$$ times the confidence between 0 and 1.

Stuck? We can help!

If you get stuck or have any questions to the online book material, the #starterkit-workshop channel in our chat is full of nice people who will provide fast help.

Refer to Collaborative Tools. for other places to get help if you have specific or detailed questions about your own analysis.

Improving things!

If you know how to do it, we recommend you to report bugs and other requests with JIRA. Make sure to use the documentation-training component of the Belle II Software project.

If you just want to give very quick feedback, use the last box “Quick feedback”.

Please make sure to be as precise as possible to make it easier for us to fix things! So for example:

• typos (where?)

• missing bits of information (what?)

• bugs (what did you do? what goes wrong?)

• too hard exercises (which one?)

• etc.

If you are familiar with git and want to create your first pull request for the software, take a look at How to contribute. We’d be happy to have you on the team!

Quick feedback!

Authors of this lesson

Kilian Lieret