Belle II Software  release-05-01-25
fei.py
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
3 
4 """
5 (Semi-)Leptonic Working Group Skims for missing energy modes that use the `FullEventInterpretation` (FEI) algorithm.
6 """
7 
8 __authors__ = [
9  "Racha Cheaib",
10  "Hannah Wakeling",
11  "Phil Grace"
12 ]
13 
14 from functools import lru_cache, wraps
15 
16 import basf2 as b2
17 import fei
18 import modularAnalysis as ma
19 from skimExpertFunctions import BaseSkim, _sphinxify_decay, fancy_skim_header
20 from variables import variables as vm
21 
22 __liaison__ = "Shanette De La Motte <shanette.delamotte@adelaide.edu.au>"
23 
24 
25 def _merge_boolean_dicts(*dicts):
26  """Merge dicts of boolean, with `True` values taking precedence if values
27  differ.
28 
29  This is a utility function for combining FEI configs. It acts in the following
30  way:
31 
32  >>> d1 = {"neutralB": True, "chargedB": False, "hadronic": True}
33  >>> d2 = {"chargedB": True, "semileptonic": True}
34  >>> _merge_FEI_configs(d1, d2)
35  {"chargedB": True, "hadronic": True, "neutralB": True, "semileptonic": True}
36 
37  Parameters:
38  dicts (dict(str -> bool)): Any number of dicts of keyword-boolean pairs.
39 
40  Returns:
41  merged (dict(str -> bool)): A single dict, containing all the keys of the
42  input dicts.
43  """
44  keys = {k for d in dicts for k in d}
45  occurances = {k: [d for d in dicts if k in d] for k in keys}
46  merged = {k: any(d[k] for d in occurances[k]) for k in keys}
47 
48  # Sort the merged dict before returning
49  merged = dict(sorted(merged.items()))
50 
51  return merged
52 
53 
54 def _get_fei_channel_names(particleName, **kwargs):
55  """Create a list containing the decay strings of all decay channels available to a
56  particle. Any keyword arguments are passed to `fei.get_default_channels`.
57 
58  This is a utility function for autogenerating FEI skim documentation.
59 
60  Args:
61  particleName (str): the PDG name of a particle, e.g. ``'K+'``, ``'pi-'``, ``'D*0'``.
62  """
63  particleList = fei.get_default_channels(**kwargs)
64  particleDict = {particle.name: particle for particle in particleList}
65 
66  try:
67  particle = particleDict[particleName]
68  except KeyError:
69  print(f"Error! Couldn't find particle with name {particleName}")
70  return []
71 
72  channels = [channel.decayString for channel in particle.channels]
73  return channels
74 
75 
76 def _hash_dict(func):
77  """Wrapper for `functools.lru_cache` to deal with dictionaries. Dictionaries are
78  mutable, so cannot be cached. This wrapper turns all dict arguments into a hashable
79  dict type, so we can use caching.
80  """
81  class HashableDict(dict):
82  def __hash__(self):
83  return hash(frozenset(self.items()))
84 
85  @wraps(func)
86  def wrapped(*args, **kwargs):
87  args = tuple([HashableDict(arg) if isinstance(arg, dict) else arg for arg in args])
88  kwargs = {k: HashableDict(v) if isinstance(v, dict) else v for k, v in kwargs.items()}
89  return func(*args, **kwargs)
90  return wrapped
91 
92 
94  """Base class for FEI skims. Applies event-level pre-cuts and applies the FEI."""
95 
96  __authors__ = ["Racha Cheaib", "Hannah Wakeling", "Phil Grace"]
97  __contact__ = __liaison__
98  __category__ = "physics, Full Event Interpretation"
99 
100  FEIPrefix = "FEIv4_2021_MC14_release_05_01_12"
101  """Prefix label for the FEI training used in the FEI skims."""
102 
103  FEIChannelArgs = {}
104  """Dict of ``str -> bool`` pairs to be passed to `fei.get_default_channels`. When
105  inheriting from `BaseFEISkim`, override this value to apply the FEI for only *e.g.*
106  SL charged :math:`B`'s."""
107 
108  MergeDataStructures = {"FEIChannelArgs": _merge_boolean_dicts}
109 
110  NoisyModules = ["ParticleCombiner"]
111 
112  ApplyHLTHadronCut = True
113  produce_on_tau_samples = False # retention is very close to zero on taupair
114 
115  @staticmethod
116  @lru_cache()
117  def fei_precuts(path):
118  """
119  Skim pre-cuts are applied before running the FEI, to reduce computation time.
120  This setup function is run by all FEI skims, so they all have the save
121  event-level pre-cuts:
122 
123  * :math:`n_{\\text{cleaned tracks}} \\geq 3`
124  * :math:`n_{\\text{cleaned ECL clusters}} \\geq 3`
125  * :math:`\\text{Visible energy of event (CMS frame)}>4~{\\rm GeV}`
126  * :math:`2~{\\rm GeV}<E_{\\text{cleaned tracks & clusters in
127  ECL}}<7~{\\rm GeV}`
128 
129  We define "cleaned" tracks and clusters as:
130 
131  * Cleaned tracks (``pi+:FEI_cleaned``): :math:`d_0 < 0.5~{\\rm cm}`,
132  :math:`|z_0| < 2~{\\rm cm}`, and :math:`p_T > 0.1~{\\rm GeV}` * Cleaned ECL
133  clusters (``gamma:FEI_cleaned``): :math:`0.296706 < \\theta < 2.61799`, and
134  :math:`E>0.1~{\\rm GeV}`
135  """
136 
137  # Pre-selection cuts
138  CleanedTrackCuts = "abs(z0) < 2.0 and abs(d0) < 0.5 and pt > 0.1"
139  CleanedClusterCuts = "E > 0.1 and 0.296706 < theta < 2.61799"
140 
141  ma.fillParticleList(decayString="pi+:FEI_cleaned",
142  cut=CleanedTrackCuts, path=path)
143  ma.fillParticleList(decayString="gamma:FEI_cleaned",
144  cut=CleanedClusterCuts, path=path)
145 
146  ma.buildEventKinematics(inputListNames=["pi+:FEI_cleaned",
147  "gamma:FEI_cleaned"],
148  path=path)
149 
150  EventCuts = " and ".join(
151  [
152  f"nCleanedTracks({CleanedTrackCuts})>=3",
153  f"nCleanedECLClusters({CleanedClusterCuts})>=3",
154  "visibleEnergyOfEventCMS>4",
155  "2<E_ECL_FEI<7",
156  ]
157  )
158 
159  # NOTE: The FEI skims are somewhat complicated, and require some manual handling
160  # of conditional paths to avoid adding the FEI to the path twice. In general, DO
161  # NOT do this kind of path handling in your own skim. Instead, use:
162  # >>> path = self.skim_event_cuts(EventLevelCuts, path=path)
163  ConditionalPath = b2.Path()
164  eselect = path.add_module("VariableToReturnValue", variable=f"passesEventCut({EventCuts})")
165  eselect.if_value('=1', ConditionalPath, b2.AfterConditionPath.CONTINUE)
166 
167  return ConditionalPath
168 
169  # This is a cached static method so that we can avoid adding FEI path twice.
170  # In combined skims, FEIChannelArgs must be combined across skims first, so that all
171  # the required particles are included in the FEI.
172  @staticmethod
173  @_hash_dict
174  @lru_cache()
175  def run_fei_for_skims(FEIChannelArgs, FEIPrefix, *, path):
176  """Reconstruct hadronic and semileptonic :math:`B^0` and :math:`B^+` tags using
177  the generically trained FEI.
178 
179  Parameters:
180  FEIChannelArgs (dict(str, bool)): A dict of keyword-boolean pairs to be
181  passed to `fei.get_default_channels`.
182  FEIPrefix (str): Prefix label for the FEI training used in the FEI skims.
183  path (`basf2.Path`): The skim path to be processed.
184  """
185  # Run FEI
186  b2.conditions.prepend_globaltag(ma.getAnalysisGlobaltag())
187  particles = fei.get_default_channels(**FEIChannelArgs)
188  configuration = fei.config.FeiConfiguration(
189  prefix=FEIPrefix,
190  training=False,
191  monitor=False)
192  feistate = fei.get_path(particles, configuration)
193  path.add_path(feistate.path)
194 
195  @staticmethod
196  @_hash_dict
197  @lru_cache()
198  def setup_fei_aliases(FEIChannelArgs):
199  # Aliases for pre-FEI event-level cuts
200  vm.addAlias("E_ECL_pi_FEI",
201  "totalECLEnergyOfParticlesInList(pi+:FEI_cleaned)")
202  vm.addAlias("E_ECL_gamma_FEI",
203  "totalECLEnergyOfParticlesInList(gamma:FEI_cleaned)")
204  vm.addAlias("E_ECL_FEI", "formula(E_ECL_pi_FEI+E_ECL_gamma_FEI)")
205 
206  # Aliases for variables available after running the FEI
207  vm.addAlias("sigProb", "extraInfo(SignalProbability)")
208  vm.addAlias("log10_sigProb", "log10(extraInfo(SignalProbability))")
209  vm.addAlias("dmID", "extraInfo(decayModeID)")
210  vm.addAlias("decayModeID", "extraInfo(decayModeID)")
211 
212  if "semileptonic" in FEIChannelArgs and FEIChannelArgs["semileptonic"]:
213  # Aliases specific to SL FEI
214  vm.addAlias("cosThetaBY", "cosThetaBetweenParticleAndNominalB")
215  vm.addAlias("d1_p_CMSframe", "useCMSFrame(daughter(1,p))")
216  vm.addAlias("d2_p_CMSframe", "useCMSFrame(daughter(2,p))")
217  vm.addAlias(
218  "p_lepton_CMSframe",
219  "conditionalVariableSelector(dmID<4, d1_p_CMSframe, d2_p_CMSframe)"
220  )
221 
222  def additional_setup(self, path):
223  """Apply pre-FEI event-level cuts and apply the FEI. This setup function is run
224  by all FEI skims, so they all have the save event-level pre-cuts.
225 
226  This function passes `FEIChannelArgs` to the cached function `run_fei_for_skims`
227  to avoid applying the FEI twice.
228 
229  See also:
230  `fei_precuts` for event-level cut definitions.
231  """
233  path = self.fei_precuts(path)
234  # The FEI skims require some manual handling of paths that is not necessary in
235  # any other skim.
236  self._ConditionalPath = path
237 
238  self.run_fei_for_skims(self.FEIChannelArgs, self.FEIPrefix, path=path)
239 
240 
241 def _FEI_skim_header(ParticleNames):
242  """Decorator factory for applying the `fancy_skim_header` header and replacing
243  <CHANNELS> in the class docstring with a list of FEI channels.
244 
245  The list is numbered with all of the corresponding decay mode IDs, and the decay
246  modes are formatted in beautiful LaTeX.
247 
248  .. code-block:: python
249 
250  @FEI_skim_header("B0")
251  class feiSLB0(BaseFEISkim):
252  # docstring here including the string '<CHANNELS>' somewhere
253 
254  Parameters:
255  ParticleNames (str, list(str)): One of either ``B0`` or ``B+``, or a list of both.
256  """
257 
258  def decorator(SkimClass):
259  if isinstance(ParticleNames, str):
260  particles = [ParticleNames]
261  else:
262  particles = ParticleNames
263 
264  ChannelsString = "List of reconstructed channels and corresponding decay mode IDs:"
265  for particle in particles:
266  channels = _get_fei_channel_names(particle, **SkimClass.FEIChannelArgs)
267  FormattedChannels = [_sphinxify_decay(channel) for channel in channels]
268  ChannelList = "\n".join(
269  [f" {dmID}. {channel}"
270  for (dmID, channel) in enumerate(FormattedChannels)]
271  )
272  if len(particles) == 1:
273  ChannelsString += "\n\n" + ChannelList
274  else:
275  ChannelsString += f"\n\n ``{particle}`` channels:\n\n" + ChannelList
276 
277  if SkimClass.__doc__ is None:
278  return SkimClass
279  else:
280  SkimClass.__doc__ = SkimClass.__doc__.replace("<CHANNELS>", ChannelsString)
281 
282  return fancy_skim_header(SkimClass)
283 
284  return decorator
285 
286 
287 @_FEI_skim_header("B0")
289  """
290  Tag side :math:`B` cuts:
291 
292  * :math:`M_{\\text{bc}} > 5.24~{\\rm GeV}`
293  * :math:`|\\Delta E| < 0.2~{\\rm GeV}`
294  * :math:`\\text{signal probability} > 0.001` (omitted for decay mode 23)
295 
296  All available FEI :math:`B^0` hadronic tags are reconstructed. From `Thomas Keck's
297  thesis <https://docs.belle2.org/record/275/files/BELLE2-MTHESIS-2015-001.pdf>`_,
298  "the channel :math:`B^0 \\to \\overline{D}^0 \\pi^0` was used by the FR, but is not
299  yet used in the FEI due to unexpected technical restrictions in the KFitter
300  algorithm".
301 
302  <CHANNELS>
303 
304  See also:
305  `BaseFEISkim.FEIPrefix` for FEI training used, and `BaseFEISkim.fei_precuts` for
306  event-level cuts made before applying the FEI.
307  """
308  __description__ = "FEI-tagged neutral :math:`B`'s decaying hadronically."
309 
310  FEIChannelArgs = {
311  "neutralB": True,
312  "chargedB": False,
313  "hadronic": True,
314  "semileptonic": False,
315  "KLong": False,
316  "baryonic": True
317  }
318 
319  def build_lists(self, path):
320  ma.applyCuts("B0:generic", "Mbc>5.24", path=path)
321  ma.applyCuts("B0:generic", "abs(deltaE)<0.200", path=path)
322  ma.applyCuts("B0:generic", "sigProb>0.001 or extraInfo(dmID)==23", path=path)
323 
324  self.SkimLists = ["B0:generic"]
325 
326  def validation_histograms(self, path):
327  # NOTE: the validation package is not part of the light releases, so this import
328  # must be made here rather than at the top of the file.
329  from validation_tools.metadata import create_validation_histograms
330 
331  vm.addAlias("d0_massDiff", "daughter(0,massDifference(0))")
332  vm.addAlias("d0_M", "daughter(0,M)")
333  vm.addAlias("nDaug", "countDaughters(1>0)") # Dummy cut so all daughters are selected.
334 
335  histogramFilename = f"{self}_Validation.root"
336  email = "Phil Grace <philip.grace@adelaide.edu.au>"
337  SkimList = self.SkimLists[0]
338 
339  variables_1d = [
340  ("sigProb", 100, 0.0, 1.0, "Signal probability", email,
341  "Signal probability of the reconstructed tag B candidates",
342  "Most around zero, with a tail at non-zero values.", "Signal probability", "Candidates", "logy"),
343  ("nDaug", 6, 0.0, 6, "Number of daughters of tag B", email,
344  "Number of daughters of tag B", "Some distribution of number of daughters", "n_{daughters}", "Candidates"),
345  ("d0_massDiff", 100, 0.0, 0.5, "Mass difference of D* and D", email,
346  "Mass difference of D^{*} and D", "Peak at 0.14 GeV", "m(D^{*})-m(D) [GeV]", "Candidates", "shifter"),
347  ("d0_M", 100, 0.0, 3.0, "Mass of zeroth daughter (D* or D)", email,
348  "Mass of zeroth daughter of tag B (either a $D^{*}$ or a D)", "Peaks at 1.86 GeV and 2.00 GeV",
349  "m(D^{(*)}) [GeV]", "Candidates", "shifter"),
350  ("deltaE", 100, -0.2, 0.2, "#Delta E", email,
351  "$\\Delta E$ of event", "Peak around zero", "#Delta E [GeV]", "Candidates"),
352  ("Mbc", 100, 5.2, 5.3, "Mbc", email,
353  "Beam-constrained mass of event", "Peaking around B mass (5.28 GeV)", "M_{bc} [GeV]", "Candidates")
354  ]
355 
356  variables_2d = [
357  ("deltaE", 100, -0.2, 0.2, "Mbc", 100, 5.2, 5.3, "Mbc vs deltaE", email,
358  "Plot of the $\\Delta E$ of the event against the beam constrained mass",
359  "Peak of $\\Delta E$ around zero, and $M_{bc}$ around B mass (5.28 GeV)",
360  "#Delta E [GeV]", "M_{bc} [GeV]", "colz, shifter"),
361  ("decayModeID", 26, 0, 26, "log10_sigProb", 100, -3.0, 0.0,
362  "Signal probability for each decay mode ID", email,
363  "Signal probability for each decay mode ID",
364  "Some distribtuion of candidates in the first few decay mode IDs",
365  "Decay mode ID", "#log_10(signal probability)", "colz")
366  ]
367 
368  create_validation_histograms(
369  rootfile=histogramFilename,
370  particlelist=SkimList,
371  variables_1d=variables_1d,
372  variables_2d=variables_2d,
373  path=path
374  )
375 
376 
377 @_FEI_skim_header("B+")
379  """
380  Tag side :math:`B` cuts:
381 
382  * :math:`M_{\\text{bc}} > 5.24~{\\rm GeV}`
383  * :math:`|\\Delta E| < 0.2~{\\rm GeV}`
384  * :math:`\\text{signal probability} > 0.001` (omitted for decay mode 25)
385 
386  All available FEI :math:`B^+` hadronic tags are reconstructed.
387 
388  <CHANNELS>
389 
390  See also:
391  `BaseFEISkim.FEIPrefix` for FEI training used, and `BaseFEISkim.fei_precuts` for
392  event-level cuts made before applying the FEI.
393  """
394  __description__ = "FEI-tagged charged :math:`B`'s decaying hadronically."
395 
396  FEIChannelArgs = {
397  "neutralB": False,
398  "chargedB": True,
399  "hadronic": True,
400  "semileptonic": False,
401  "KLong": False,
402  "baryonic": True
403  }
404 
405  def build_lists(self, path):
406  ma.applyCuts("B+:generic", "Mbc>5.24", path=path)
407  ma.applyCuts("B+:generic", "abs(deltaE)<0.200", path=path)
408  ma.applyCuts("B+:generic", "sigProb>0.001 or extraInfo(dmID)==25", path=path)
409 
410  self.SkimLists = ["B+:generic"]
411 
412  def validation_histograms(self, path):
413  # NOTE: the validation package is not part of the light releases, so this import
414  # must be made here rather than at the top of the file.
415  from validation_tools.metadata import create_validation_histograms
416 
417  vm.addAlias("d0_massDiff", "daughter(0,massDifference(0))")
418  vm.addAlias("d0_M", "daughter(0,M)")
419  vm.addAlias("nDaug", "countDaughters(1>0)") # Dummy cut so all daughters are selected.
420 
421  histogramFilename = f"{self}_Validation.root"
422  email = "Phil Grace <philip.grace@adelaide.edu.au>"
423  SkimList = self.SkimLists[0]
424 
425  variables_1d = [
426  ("sigProb", 100, 0.0, 1.0, "Signal probability", email,
427  "Signal probability of the reconstructed tag B candidates",
428  "Most around zero, with a tail at non-zero values.", "Signal probability", "Candidates", "logy"),
429  ("nDaug", 6, 0.0, 6, "Number of daughters of tag B", email,
430  "Number of daughters of tag B", "Some distribution of number of daughters", "n_{daughters}", "Candidates"),
431  ("d0_massDiff", 100, 0.0, 0.5, "Mass difference of D* and D", email,
432  "Mass difference of D^{*} and D", "Peak at 0.14 GeV", "m(D^{*})-m(D) [GeV]", "Candidates", "shifter"),
433  ("d0_M", 100, 0.0, 3.0, "Mass of zeroth daughter (D* or D)", email,
434  "Mass of zeroth daughter of tag B (either a $D^{*}$ or a D)", "Peaks at 1.86 GeV and 2.00 GeV",
435  "m(D^{(*)}) [GeV]", "Candidates", "shifter"),
436  ("deltaE", 100, -0.2, 0.2, "#Delta E", email,
437  "$\\Delta E$ of event", "Peak around zero", "#Delta E [GeV]", "Candidates"),
438  ("Mbc", 100, 5.2, 5.3, "Mbc", email,
439  "Beam-constrained mass of event", "Peaking around B mass (5.28 GeV)", "M_{bc} [GeV]", "Candidates")
440  ]
441 
442  variables_2d = [
443  ("deltaE", 100, -0.2, 0.2, "Mbc", 100, 5.2, 5.3, "Mbc vs deltaE", email,
444  "Plot of the $\\Delta E$ of the event against the beam constrained mass",
445  "Peak of $\\Delta E$ around zero, and $M_{bc}$ around B mass (5.28 GeV)",
446  "#Delta E [GeV]", "M_{bc} [GeV]", "colz, shifter"),
447  ("decayModeID", 29, 0, 29, "log10_sigProb", 100, -3.0, 0.0,
448  "Signal probability for each decay mode ID", email,
449  "Signal probability for each decay mode ID",
450  "Some distribtuion of candidates in the first few decay mode IDs",
451  "Decay mode ID", "#log_10(signal probability)", "colz")
452  ]
453 
454  create_validation_histograms(
455  rootfile=histogramFilename,
456  particlelist=SkimList,
457  variables_1d=variables_1d,
458  variables_2d=variables_2d,
459  path=path
460  )
461 
462 
463 @_FEI_skim_header("B0")
465  """
466  Tag side :math:`B` cuts:
467 
468  * :math:`-4 < \\cos\\theta_{BY} < 3`
469  * :math:`\\log_{10}(\\text{signal probability}) > -2.4`
470  * :math:`p_{\\ell}^{*} > 1.0~{\\rm GeV}` in CMS frame
471 
472  SL :math:`B^0` tags are reconstructed. Hadronic :math:`B` with SL :math:`D` are not
473  reconstructed, as these are rare and time-intensive.
474 
475  <CHANNELS>
476 
477  See also:
478  `BaseFEISkim.FEIPrefix` for FEI training used, and `BaseFEISkim.fei_precuts` for
479  event-level cuts made before applying the FEI.
480  """
481  __description__ = "FEI-tagged neutral :math:`B`'s decaying semileptonically."
482 
483  FEIChannelArgs = {
484  "neutralB": True,
485  "chargedB": False,
486  "hadronic": False,
487  "semileptonic": True,
488  "KLong": False,
489  "baryonic": True,
490  "removeSLD": True
491  }
492 
493  def build_lists(self, path):
494  ma.applyCuts("B0:semileptonic", "dmID<8", path=path)
495  ma.applyCuts("B0:semileptonic", "log10(sigProb)>-2.4", path=path)
496  ma.applyCuts("B0:semileptonic", "-4.0<cosThetaBY<3.0", path=path)
497  ma.applyCuts("B0:semileptonic", "p_lepton_CMSframe>1.0", path=path)
498 
499  self.SkimLists = ["B0:semileptonic"]
500 
501  def validation_histograms(self, path):
502  # NOTE: the validation package is not part of the light releases, so this import
503  # must be made here rather than at the top of the file.
504  from validation_tools.metadata import create_validation_histograms
505 
506  vm.addAlias("d0_massDiff", "daughter(0,massDifference(0))")
507  vm.addAlias("d0_M", "daughter(0,M)")
508  vm.addAlias("nDaug", "countDaughters(1>0)") # Dummy cut so all daughters are selected.
509 
510  histogramFilename = f"{self}_Validation.root"
511  email = "Phil Grace <philip.grace@adelaide.edu.au>"
512  SkimList = self.SkimLists[0]
513 
514  variables_1d = [
515  ("sigProb", 100, 0.0, 1.0, "Signal probability", email,
516  "Signal probability of the reconstructed tag B candidates",
517  "Most around zero, with a tail at non-zero values.",
518  "Signal probability", "Candidates", "logy"),
519  ("nDaug", 6, 0.0, 6, "Number of daughters of tag B", email,
520  "Number of daughters of tag B", "Some distribution of number of daughters",
521  "n_{daughters}", "Candidates"),
522  ("cosThetaBetweenParticleAndNominalB", 100, -6.0, 4.0, "#cos#theta_{BY}", email,
523  "Cosine of angle between the reconstructed B and the nominal B",
524  "Distribution peaking between -1 and 1", "#cos#theta_{BY}", "Candidates"),
525  ("d0_massDiff", 100, 0.0, 0.5, "Mass difference of D* and D", email,
526  "Mass difference of $D^{*}$ and D", "Peak at 0.14 GeV", "m(D^{*})-m(D) [GeV]",
527  "Candidates", "shifter"),
528  ("d0_M", 100, 0.0, 3.0, "Mass of zeroth daughter (D* or D)", email,
529  "Mass of zeroth daughter of tag B (either a $D^{*}$ or a D)", "Peaks at 1.86 GeV and 2.00 GeV",
530  "m(D^{(*)}) [GeV]", "Candidates", "shifter")
531  ]
532 
533  variables_2d = [
534  ("decayModeID", 8, 0, 8, "log10_sigProb", 100, -3.0, 0.0,
535  "Signal probability for each decay mode ID", email,
536  "Signal probability for each decay mode ID",
537  "Some distribtuion of candidates in the first few decay mode IDs",
538  "Decay mode ID", "#log_10(signal probability)", "colz")
539  ]
540 
541  create_validation_histograms(
542  rootfile=histogramFilename,
543  particlelist=SkimList,
544  variables_1d=variables_1d,
545  variables_2d=variables_2d,
546  path=path
547  )
548 
549 
550 @_FEI_skim_header("B+")
552  """
553  Tag side :math:`B` cuts:
554 
555  * :math:`-4 < \\cos\\theta_{BY} < 3`
556  * :math:`\\log_{10}(\\text{signal probability}) > -2.4`
557  * :math:`p_{\\ell}^{*} > 1.0~{\\rm GeV}` in CMS frame
558 
559  SL :math:`B^+` tags are reconstructed. Hadronic :math:`B^+` with SL :math:`D` are
560  not reconstructed, as these are rare and time-intensive.
561 
562  <CHANNELS>
563 
564  See also:
565  `BaseFEISkim.FEIPrefix` for FEI training used, and `BaseFEISkim.fei_precuts` for
566  event-level cuts made before applying the FEI.
567  """
568  __description__ = "FEI-tagged charged :math:`B`'s decaying semileptonically."
569 
570  FEIChannelArgs = {
571  "neutralB": False,
572  "chargedB": True,
573  "hadronic": False,
574  "semileptonic": True,
575  "KLong": False,
576  "baryonic": True,
577  "removeSLD": True
578  }
579 
580  def build_lists(self, path):
581  ma.applyCuts("B+:semileptonic", "dmID<8", path=path)
582  ma.applyCuts("B+:semileptonic", "log10_sigProb>-2.4", path=path)
583  ma.applyCuts("B+:semileptonic", "-4.0<cosThetaBY<3.0", path=path)
584  ma.applyCuts("B+:semileptonic", "p_lepton_CMSframe>1.0", path=path)
585 
586  self.SkimLists = ["B+:semileptonic"]
587 
588  def validation_histograms(self, path):
589  # NOTE: the validation package is not part of the light releases, so this import
590  # must be made here rather than at the top of the file.
591  from validation_tools.metadata import create_validation_histograms
592 
593  vm.addAlias("d0_massDiff", "daughter(0,massDifference(0))")
594  vm.addAlias("d0_M", "daughter(0,M)")
595  vm.addAlias("nDaug", "countDaughters(1>0)") # Dummy cut so all daughters are selected.
596 
597  histogramFilename = f"{self}_Validation.root"
598  email = "Phil Grace <philip.grace@adelaide.edu.au>"
599  SkimList = self.SkimLists[0]
600 
601  variables_1d = [
602  ("sigProb", 100, 0.0, 1.0, "Signal probability", email,
603  "Signal probability of the reconstructed tag B candidates",
604  "Most around zero, with a tail at non-zero values.",
605  "Signal probability", "Candidates", "logy"),
606  ("nDaug", 6, 0.0, 6, "Number of daughters of tag B", email,
607  "Number of daughters of tag B", "Some distribution of number of daughters",
608  "n_{daughters}", "Candidates"),
609  ("cosThetaBetweenParticleAndNominalB", 100, -6.0, 4.0, "#cos#theta_{BY}", email,
610  "Cosine of angle between the reconstructed B and the nominal B",
611  "Distribution peaking between -1 and 1", "#cos#theta_{BY}", "Candidates"),
612  ("d0_massDiff", 100, 0.0, 0.5, "Mass difference of D* and D", email,
613  "Mass difference of $D^{*}$ and D", "Peak at 0.14 GeV", "m(D^{*})-m(D) [GeV]",
614  "Candidates", "shifter"),
615  ("d0_M", 100, 0.0, 3.0, "Mass of zeroth daughter (D* or D)", email,
616  "Mass of zeroth daughter of tag B (either a $D^{*}$ or a D)", "Peaks at 1.86 GeV and 2.00 GeV",
617  "m(D^{(*)}) [GeV]", "Candidates", "shifter")
618  ]
619 
620  variables_2d = [
621  ("decayModeID", 8, 0, 8, "log10_sigProb", 100, -3.0, 0.0,
622  "Signal probability for each decay mode ID", email,
623  "Signal probability for each decay mode ID",
624  "Some distribtuion of candidates in the first few decay mode IDs",
625  "Decay mode ID", "#log_10(signal probability)", "colz")
626  ]
627 
628  create_validation_histograms(
629  rootfile=histogramFilename,
630  particlelist=SkimList,
631  variables_1d=variables_1d,
632  variables_2d=variables_2d,
633  path=path
634  )
635 
636 
637 @_FEI_skim_header(["B0", "B+"])
639  """
640  Tag side :math:`B` cuts:
641 
642  * :math:`M_{\\text{bc}} > 5.24~{\\rm GeV}`
643  * :math:`|\\Delta E| < 0.2~{\\rm GeV}`
644  * :math:`\\text{signal probability} > 0.001` (omitted for decay mode 23 for
645  :math:`B^+`, and decay mode 25 for :math:`B^0`)
646 
647  All available FEI :math:`B^0` and :math:`B^+` hadronic tags are reconstructed. From
648  `Thomas Keck's thesis
649  <https://docs.belle2.org/record/275/files/BELLE2-MTHESIS-2015-001.pdf>`_, "the
650  channel :math:`B^0 \\to \\overline{D}^0 \\pi^0` was used by the FR, but is not yet
651  used in the FEI due to unexpected technical restrictions in the KFitter algorithm".
652 
653  <CHANNELS>
654 
655  See also:
656  `BaseFEISkim.FEIPrefix` for FEI training used, and `BaseFEISkim.fei_precuts` for
657  event-level cuts made before applying the FEI.
658  """
659  __description__ = "FEI-tagged neutral and charged :math:`B`'s decaying hadronically."
660 
661  FEIChannelArgs = {
662  "neutralB": True,
663  "chargedB": True,
664  "hadronic": True,
665  "semileptonic": False,
666  "KLong": False,
667  "baryonic": True
668  }
669 
670  def build_lists(self, path):
671  ma.copyList("B0:feiHadronic", "B0:generic", path=path)
672  ma.copyList("B+:feiHadronic", "B+:generic", path=path)
673  HadronicBLists = ["B0:feiHadronic", "B+:feiHadronic"]
674 
675  for BList in HadronicBLists:
676  ma.applyCuts(BList, "Mbc>5.24", path=path)
677  ma.applyCuts(BList, "abs(deltaE)<0.200", path=path)
678 
679  ma.applyCuts("B+:feiHadronic", "sigProb>0.001 or extraInfo(dmID)==25", path=path)
680  ma.applyCuts("B0:feiHadronic", "sigProb>0.001 or extraInfo(dmID)==23", path=path)
681 
682  self.SkimLists = HadronicBLists
683 
684 
685 @_FEI_skim_header(["B0", "B+"])
687  """
688  Tag side :math:`B` cuts:
689 
690  * :math:`-4 < \\cos\\theta_{BY} < 3`
691  * :math:`\\log_{10}(\\text{signal probability}) > -2.4`
692  * :math:`p_{\\ell}^{*} > 1.0~{\\rm GeV}` in CMS frame
693 
694  SL :math:`B^0` and :math:`B^+` tags are reconstructed. Hadronic :math:`B` with SL
695  :math:`D` are not reconstructed, as these are rare and time-intensive.
696 
697  <CHANNELS>
698 
699  See also:
700  `BaseFEISkim.FEIPrefix` for FEI training used, and `BaseFEISkim.fei_precuts` for
701  event-level cuts made before applying the FEI.
702  """
703  __description__ = "FEI-tagged neutral and charged :math:`B`'s decaying semileptonically."
704 
705  FEIChannelArgs = {
706  "neutralB": True,
707  "chargedB": True,
708  "hadronic": False,
709  "semileptonic": True,
710  "KLong": False,
711  "baryonic": True,
712  "removeSLD": True
713  }
714 
715  def build_lists(self, path):
716  ma.copyList("B0:feiSL", "B0:semileptonic", path=path)
717  ma.copyList("B+:feiSL", "B+:semileptonic", path=path)
718  SLBLists = ["B0:feiSL", "B+:feiSL"]
719 
720  Bcuts = ["log10_sigProb>-2.4", "-4.0<cosThetaBY<3.0", "p_lepton_CMSframe>1.0"]
721 
722  for BList in SLBLists:
723  for cut in Bcuts:
724  ma.applyCuts(BList, cut, path=path)
725 
726  self.SkimLists = SLBLists
skim.fei.feiHadronicB0.validation_histograms
def validation_histograms(self, path)
Definition: fei.py:326
skim.fei.BaseFEISkim.setup_fei_aliases
def setup_fei_aliases(FEIChannelArgs)
Definition: fei.py:198
skim.fei.feiSLBplus.validation_histograms
def validation_histograms(self, path)
Definition: fei.py:588
skim.fei.BaseFEISkim.additional_setup
def additional_setup(self, path)
Definition: fei.py:222
skim.fei.feiSLBplus.build_lists
def build_lists(self, path)
Definition: fei.py:580
skim.fei.feiHadronic.build_lists
def build_lists(self, path)
Definition: fei.py:670
skim.fei.BaseFEISkim.run_fei_for_skims
def run_fei_for_skims(FEIChannelArgs, FEIPrefix, *path)
Definition: fei.py:175
skim.fei.feiHadronicB0.SkimLists
SkimLists
Definition: fei.py:324
skim.fei.feiSL.build_lists
def build_lists(self, path)
Definition: fei.py:715
skim.fei.feiHadronicBplus.SkimLists
SkimLists
Definition: fei.py:410
skim.fei.BaseFEISkim.fei_precuts
def fei_precuts(path)
Definition: fei.py:117
skim.fei.feiSLB0
Definition: fei.py:464
skim.fei.feiHadronicB0.build_lists
def build_lists(self, path)
Definition: fei.py:319
skim.fei.feiSLBplus
Definition: fei.py:551
skim.fei.feiSL.SkimLists
SkimLists
Definition: fei.py:726
skim.fei.feiHadronicB0
Definition: fei.py:288
skim.fei.BaseFEISkim.FEIPrefix
string FEIPrefix
Definition: fei.py:100
skim.fei.BaseFEISkim._ConditionalPath
_ConditionalPath
Definition: fei.py:236
skim.fei.feiSLB0.SkimLists
SkimLists
Definition: fei.py:499
skimExpertFunctions.BaseSkim
Definition: skimExpertFunctions.py:504
skim.fei.feiSL
Definition: fei.py:686
skim.fei.BaseFEISkim
Definition: fei.py:93
skim.fei.feiHadronicBplus.build_lists
def build_lists(self, path)
Definition: fei.py:405
skim.fei.BaseFEISkim.FEIChannelArgs
dictionary FEIChannelArgs
Definition: fei.py:103
validation_tools.metadata
Definition: metadata.py:1
skim.fei.feiSLBplus.SkimLists
SkimLists
Definition: fei.py:586
skim.fei.feiHadronic.SkimLists
SkimLists
Definition: fei.py:682
skim.fei.feiHadronic
Definition: fei.py:638
skim.fei.feiSLB0.build_lists
def build_lists(self, path)
Definition: fei.py:493
skim.fei.feiSLB0.validation_histograms
def validation_histograms(self, path)
Definition: fei.py:501
skim.fei.feiHadronicBplus.validation_histograms
def validation_histograms(self, path)
Definition: fei.py:412
skim.fei.feiHadronicBplus
Definition: fei.py:378