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