Belle II Software  release-06-01-16
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("B0")
534  """
535  Tag side :math:`B` cuts:
536 
537  * :math:`\\text{FoxWolframR2} < 0.4`
538  * :math:`-1.75 < \\cos\\theta_{BY} < 1.1`
539  * :math:`\\log_{10}(\\text{signal probability}) > -2.0`
540  * :math:`p_{\\ell}^{*} > 1.0~{\\rm GeV}` in CMS frame
541  * :math:`\\text{BCS:signal probability}`
542 
543  SL :math:`B^0` tags are reconstructed. Hadronic :math:`B` with SL :math:`D` are not
544  reconstructed, as these are rare and time-intensive.
545 
546  <CHANNELS>
547 
548  See also:
549  `BaseFEISkim.FEIPrefix` for FEI training used, and `BaseFEISkim.fei_precuts` for
550  event-level cuts made before applying the FEI.
551  """
552  __description__ = ("FEI-tagged neutral :math:`B`'s decaying semileptonically",
553  "Analysis cuts included, best sigProb candidate kept"
554  )
555  validation_sample = _VALIDATION_SAMPLE
556 
557  FEIChannelArgs = {
558  "neutralB": True,
559  "chargedB": False,
560  "hadronic": False,
561  "semileptonic": True,
562  "KLong": False,
563  "baryonic": True,
564  "removeSLD": True
565  }
566 
567  def build_lists(self, path):
568  ma.buildEventShape(inputListNames=['pi+:FEI_cleaned', 'gamma:FEI_cleaned'],
569  foxWolfram=True,
570  harmonicMoments=False,
571  cleoCones=False,
572  thrust=False,
573  collisionAxis=False,
574  jets=False,
575  sphericity=False,
576  checkForDuplicates=True,
577  path=path)
578 
579  # tightened cuts on sigprob and cosThetaBY as well as additional cut on Fox WR R2
580  vm.addAlias('foxWolframR2_maskedNaN', 'ifNANgiveX(foxWolframR2,1)')
581  TighterCuts = " and ".join(
582  [
583  "foxWolframR2_maskedNaN<0.4",
584  "dmID < 8",
585  "log10(sigProb) > -2.0",
586  "-1.75 < cosThetaBY < 1.1",
587  "p_lepton_CMSframe > 1.0"
588  ]
589  )
590  ma.cutAndCopyList("B0:SLRDstar", "B0:semileptonic", TighterCuts, path=path)
591 
592  # best candidate selection on signal probability
593  ma.rankByHighest("B0:SLRDstar", "sigProb", numBest=1,
594  allowMultiRank=True, outputVariable='sigProb_rank_tag',
595  path=path)
596 
597  return ["B0:SLRDstar"]
598 
599  def validation_histograms(self, path):
600  # NOTE: the validation package is not part of the light releases, so this import
601  # must be made here rather than at the top of the file.
602  from validation_tools.metadata import create_validation_histograms
603 
604  vm.addAlias('sigProb', 'extraInfo(SignalProbability)')
605  vm.addAlias('log10_sigProb', 'log10(extraInfo(SignalProbability))')
606  vm.addAlias('d0_massDiff', 'daughter(0,massDifference(0))')
607  vm.addAlias('d0_M', 'daughter(0,M)')
608  vm.addAlias('decayModeID', 'extraInfo(decayModeID)')
609  vm.addAlias('nDaug', 'countDaughters(1>0)') # Dummy cut so all daughters are selected.
610 
611  histogramFilename = f"{self}_Validation.root"
612 
613  create_validation_histograms(
614  rootfile=histogramFilename,
615  particlelist='B0:SLRDstar',
616  variables_1d=[
617  ('sigProb', 100, 0.0, 1.0, 'Signal probability', __liaison__,
618  'Signal probability of the reconstructed tag B candidates', 'Most around zero, with a tail at non-zero values.',
619  'Signal probability', 'Candidates', 'logy'),
620  ('nDaug', 6, 0.0, 6, 'Number of daughters of tag B', __liaison__,
621  'Number of daughters of tag B', 'Some distribution of number of daughters', 'n_{daughters}', 'Candidates'),
622  ('cosThetaBetweenParticleAndNominalB', 100, -6.0, 4.0, '#cos#theta_{BY}', __liaison__,
623  'Cosine of angle between the reconstructed B and the nominal B', 'Distribution peaking between -1 and 1',
624  '#cos#theta_{BY}', 'Candidates'),
625  ('d0_massDiff', 100, 0.0, 0.5, 'Mass difference of D* and D', __liaison__,
626  'Mass difference of $D^{*}$ and D', 'Peak at 0.14 GeV', 'm(D^{*})-m(D) [GeV]', 'Candidates', 'shifter'),
627  ('d0_M', 100, 0.0, 3.0, 'Mass of zeroth daughter (D* or D)', __liaison__,
628  'Mass of zeroth daughter of tag B (either a $D^{*}$ or a D)', 'Peaks at 1.86 GeV and 2.00 GeV',
629  'm(D^{(*)}) [GeV]', 'Candidates', 'shifter')],
630  variables_2d=[('decayModeID', 8, 0, 8, 'log10_sigProb', 100, -3.0, 0.0,
631  'Signal probability for each decay mode ID', __liaison__,
632  'Signal probability for each decay mode ID',
633  'Some distribution of candidates in the first few decay mode IDs',
634  'Decay mode ID', '#log_10(signal probability)', 'colz')],
635  path=path)
636 
637 
638 @_FEI_skim_header("B+")
640  """
641  Tag side :math:`B` cuts:
642 
643  * :math:`-4 < \\cos\\theta_{BY} < 3`
644  * :math:`\\log_{10}(\\text{signal probability}) > -2.4`
645  * :math:`p_{\\ell}^{*} > 1.0~{\\rm GeV}` in CMS frame
646 
647  SL :math:`B^+` tags are reconstructed. Hadronic :math:`B^+` with SL :math:`D` are
648  not reconstructed, as these are rare and time-intensive.
649 
650  <CHANNELS>
651 
652  See also:
653  `BaseFEISkim.FEIPrefix` for FEI training used, and `BaseFEISkim.fei_precuts` for
654  event-level cuts made before applying the FEI.
655  """
656  __description__ = "FEI-tagged charged :math:`B`'s decaying semileptonically."
657  validation_sample = _VALIDATION_SAMPLE
658 
659  FEIChannelArgs = {
660  "neutralB": False,
661  "chargedB": True,
662  "hadronic": False,
663  "semileptonic": True,
664  "KLong": False,
665  "baryonic": True,
666  "removeSLD": True
667  }
668 
669  def build_lists(self, path):
670  ma.applyCuts("B+:semileptonic", "dmID<8", path=path)
671  ma.applyCuts("B+:semileptonic", "log10_sigProb>-2.4", path=path)
672  ma.applyCuts("B+:semileptonic", "-4.0<cosThetaBY<3.0", path=path)
673  ma.applyCuts("B+:semileptonic", "p_lepton_CMSframe>1.0", path=path)
674 
675  return ["B+:semileptonic"]
676 
677  def validation_histograms(self, path):
678  # NOTE: the validation package is not part of the light releases, so this import
679  # must be made here rather than at the top of the file.
680  from validation_tools.metadata import create_validation_histograms
681 
682  vm.addAlias('sigProb', 'extraInfo(SignalProbability)')
683  vm.addAlias('log10_sigProb', 'log10(extraInfo(SignalProbability))')
684  vm.addAlias('d0_massDiff', 'daughter(0,massDifference(0))')
685  vm.addAlias('d0_M', 'daughter(0,M)')
686  vm.addAlias('decayModeID', 'extraInfo(decayModeID)')
687  vm.addAlias('nDaug', 'countDaughters(1>0)') # Dummy cut so all daughters are selected.
688 
689  histogramFilename = f"{self}_Validation.root"
690 
691  create_validation_histograms(
692  rootfile=histogramFilename,
693  particlelist='B+:semileptonic',
694  variables_1d=[
695  ('sigProb', 100, 0.0, 1.0, 'Signal probability', __liaison__,
696  'Signal probability of the reconstructed tag B candidates',
697  'Most around zero, with a tail at non-zero values.', 'Signal probability', 'Candidates', 'logy'),
698  ('nDaug', 6, 0.0, 6, 'Number of daughters of tag B', __liaison__,
699  'Number of daughters of tag B', 'Some distribution of number of daughters', 'n_{daughters}', 'Candidates'),
700  ('cosThetaBetweenParticleAndNominalB', 100, -6.0, 4.0, '#cos#theta_{BY}', __liaison__,
701  'Cosine of angle between the reconstructed B and the nominal B', 'Distribution peaking between -1 and 1',
702  '#cos#theta_{BY}', 'Candidates'),
703  ('d0_massDiff', 100, 0.0, 0.5, 'Mass difference of D* and D', __liaison__,
704  'Mass difference of $D^{*}$ and D', 'Peak at 0.14 GeV', 'm(D^{*})-m(D) [GeV]', 'Candidates', 'shifter'),
705  ('d0_M', 100, 0.0, 3.0, 'Mass of zeroth daughter (D* or D)', __liaison__,
706  'Mass of zeroth daughter of tag B (either a $D^{*}$ or a D)', 'Peaks at 1.86 GeV and 2.00 GeV',
707  'm(D^{(*)}) [GeV]', 'Candidates', 'shifter')],
708  variables_2d=[('decayModeID', 8, 0, 8, 'log10_sigProb', 100, -3.0, 0.0,
709  'Signal probability for each decay mode ID', __liaison__,
710  'Signal probability for each decay mode ID',
711  'Some distribtuion of candidates in the first few decay mode IDs',
712  'Decay mode ID', '#log_10(signal probability)', 'colz')],
713  path=path)
714 
715 
716 @_FEI_skim_header(["B0", "B+"])
718  """
719  Tag side :math:`B` cuts:
720 
721  * :math:`M_{\\text{bc}} > 5.2~{\\rm GeV}`
722  * :math:`|\\Delta E| < 0.3~{\\rm GeV}`
723  * :math:`\\text{signal probability} > 0.001` (omitted for decay mode 23 for
724  :math:`B^+`, and decay mode 25 for :math:`B^0`)
725 
726  All available FEI :math:`B^0` and :math:`B^+` hadronic tags are reconstructed. From
727  `Thomas Keck's thesis
728  <https://docs.belle2.org/record/275/files/BELLE2-MTHESIS-2015-001.pdf>`_, "the
729  channel :math:`B^0 \\to \\overline{D}^0 \\pi^0` was used by the FR, but is not yet
730  used in the FEI due to unexpected technical restrictions in the KFitter algorithm".
731 
732  <CHANNELS>
733 
734  See also:
735  `BaseFEISkim.FEIPrefix` for FEI training used, and `BaseFEISkim.fei_precuts` for
736  event-level cuts made before applying the FEI.
737  """
738  __description__ = "FEI-tagged neutral and charged :math:`B`'s decaying hadronically."
739 
740  FEIChannelArgs = {
741  "neutralB": True,
742  "chargedB": True,
743  "hadronic": True,
744  "semileptonic": False,
745  "KLong": False,
746  "baryonic": True
747  }
748 
749  def build_lists(self, path):
750  ma.copyList("B0:feiHadronic", "B0:generic", path=path)
751  ma.copyList("B+:feiHadronic", "B+:generic", path=path)
752  HadronicBLists = ["B0:feiHadronic", "B+:feiHadronic"]
753 
754  for BList in HadronicBLists:
755  ma.applyCuts(BList, "Mbc>5.2", path=path)
756  ma.applyCuts(BList, "abs(deltaE)<0.300", path=path)
757 
758  ma.applyCuts("B+:feiHadronic", "sigProb>0.001 or extraInfo(dmID)==25", path=path)
759  ma.applyCuts("B0:feiHadronic", "sigProb>0.001 or extraInfo(dmID)==23", path=path)
760 
761  return HadronicBLists
762 
763 
764 @_FEI_skim_header(["B0", "B+"])
766  """
767  Tag side :math:`B` cuts:
768 
769  * :math:`M_{\\text{bc}} > 5.27~{\\rm GeV}`
770  * :math:`-0.150 < \\Delta E < 0.100~{\\rm GeV}`
771  * :math:`\\text{signal probability} > 0.001` (omitted for decay mode 23 for
772  :math:`B^+`, and decay mode 25 for :math:`B^0`)
773  * :math:`\\cos{TBTO} < 0.9`
774  * Selects only the two best candidates that survive based on the signalProbability
775 
776  All available FEI :math:`B^0` and :math:`B^+` hadronic tags are reconstructed. From
777  `Thomas Keck's thesis
778  <https://docs.belle2.org/record/275/files/BELLE2-MTHESIS-2015-001.pdf>`_, "the
779  channel :math:`B^0 \\to \\overline{D}^0 \\pi^0` was used by the FR, but is not yet
780  used in the FEI due to unexpected technical restrictions in the KFitter algorithm".
781 
782  <CHANNELS>
783 
784  See also:
785  `BaseFEISkim.FEIPrefix` for FEI training used, and `BaseFEISkim.fei_precuts` for
786  event-level cuts made before applying the FEI.
787  """
788 
789  __description__ = ("FEI-tagged neutral and charged :math:`B`'s decaying hadronically. "
790  "Analysis specific cuts applied during skimming. Best 2 candidates ranked by sigProb kept"
791  )
792 
793  FEIChannelArgs = {
794  "neutralB": True,
795  "chargedB": True,
796  "hadronic": True,
797  "semileptonic": False,
798  "KLong": False,
799  "baryonic": True,
800  }
801 
802  def build_lists(self, path):
803  ma.copyList("B0:feiHadronicDstEllNu", "B0:generic", path=path)
804  ma.copyList("B+:feiHadronicDstEllNu", "B+:generic", path=path)
805  HadronicBLists = ["B0:feiHadronicDstEllNu", "B+:feiHadronicDstEllNu"]
806 
807  ma.applyCuts(
808  "B+:feiHadronicDstEllNu", "sigProb>0.001 or extraInfo(dmID)==25", path=path
809  )
810  ma.applyCuts(
811  "B0:feiHadronicDstEllNu", "sigProb>0.001 or extraInfo(dmID)==23", path=path
812  )
813 
814  for BList in HadronicBLists:
815  ma.applyCuts(BList, "Mbc>5.27", path=path)
816  ma.applyCuts(BList, "-0.150 <= deltaE <= 0.100", path=path)
817  # Need to build Btag_ROE to build continuum suppression variables i.e. cosTBTO
818  self._build_continuum_suppression_build_continuum_suppression(particle_list=BList, path=path)
819  ma.applyCuts(BList, "cosTBTO < 0.9", path=path)
820  # Keep only the best 2 candidates that survive based on the Signal probability.
821  # The second candidate is kept just in case an analyst wishes to
822  # perform some tag sipyde studies
823  ma.rankByHighest(
824  particleList=BList,
825  variable="extraInfo(SignalProbability)",
826  numBest=2,
827  path=path,
828  )
829 
830  return HadronicBLists
831 
832  def _build_continuum_suppression(self, particle_list: str, path: b2.Path):
833  """Builds continuum suppression for a given b-meson list.
834  This module is required to save the CS variables.
835 
836  Args:2
837  list_name (str) : name of the b meson list. Can be list of signal or tag b meson lists
838  path (b2.Path): the basf2 path to append modules
839 
840 
841  """
842 
843  mask_name = "btag_cs"
844 
845  # First build the rest of the event for the b meson
846  ma.buildRestOfEvent(target_list_name=particle_list, path=path)
847 
848  # Append the ROE mask
849  ma.appendROEMasks(
850  particle_list,
851  [self._build_btag_roe_mask_build_btag_roe_mask(mask_name=mask_name)],
852  path=path,
853  )
854  # Build the continuum object for the given b meson
855  ma.buildContinuumSuppression(
856  list_name=particle_list, roe_mask=mask_name, path=path
857  )
858 
859  @staticmethod
860  def _build_btag_roe_mask(mask_name: str):
861  """Prepares a tuple with the ROE mask name and the ROE cuts for tracks and clusters
862  The actual cuts are read from a selections dictionary
863 
864  The cuts are aligning with the continuum rejection that is applied for the centrally
865  produced FEI calibration
866 
867  Args:
868  mask_name (str): The name of the mask. This is the name that will be used to append the mask
869 
870  Return:
871  tuple of mask_name, tracking cuts and cluster_cuts
872  """
873 
874  selections = {
875  "tracks": [
876  "abs(dr) < 2",
877  "abs(dz) < 4",
878  "pt > 0.2",
879  "thetaInCDCAcceptance == 1",
880  ],
881  "clusters": [
882  "clusterNHits > 1.5",
883  "[[clusterReg==1 and E > 0.080] or [clusterReg==2 and E > 0.030] or [clusterReg==3 and E > 0.060]]",
884  "[[clusterTheta > 0.2967] and [clusterTheta < 2.6180]]",
885  "abs(clusterTiming) < 200",
886  ],
887  }
888 
889  track_cuts = "".join(["[", " and ".join(selections["tracks"]), "]"])
890  cluster_cuts = "".join(["[", " and ".join(selections["clusters"]), "]"])
891 
892  return (mask_name, track_cuts, cluster_cuts)
893 
894 
895 @_FEI_skim_header(["B0", "B+"])
897  """
898  Tag side :math:`B` cuts:
899 
900  * :math:`-4 < \\cos\\theta_{BY} < 3`
901  * :math:`\\log_{10}(\\text{signal probability}) > -2.4`
902  * :math:`p_{\\ell}^{*} > 1.0~{\\rm GeV}` in CMS frame
903 
904  SL :math:`B^0` and :math:`B^+` tags are reconstructed. Hadronic :math:`B` with SL
905  :math:`D` are not reconstructed, as these are rare and time-intensive.
906 
907  <CHANNELS>
908 
909  See also:
910  `BaseFEISkim.FEIPrefix` for FEI training used, and `BaseFEISkim.fei_precuts` for
911  event-level cuts made before applying the FEI.
912  """
913  __description__ = "FEI-tagged neutral and charged :math:`B`'s decaying semileptonically."
914 
915  FEIChannelArgs = {
916  "neutralB": True,
917  "chargedB": True,
918  "hadronic": False,
919  "semileptonic": True,
920  "KLong": False,
921  "baryonic": True,
922  "removeSLD": True
923  }
924 
925  def build_lists(self, path):
926  ma.copyList("B0:feiSL", "B0:semileptonic", path=path)
927  ma.copyList("B+:feiSL", "B+:semileptonic", path=path)
928  SLBLists = ["B0:feiSL", "B+:feiSL"]
929 
930  Bcuts = ["log10_sigProb>-2.4", "-4.0<cosThetaBY<3.0", "p_lepton_CMSframe>1.0"]
931 
932  for BList in SLBLists:
933  for cut in Bcuts:
934  ma.applyCuts(BList, cut, path=path)
935 
936  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_btag_roe_mask(str mask_name)
Definition: fei.py:860
def build_lists(self, path)
Definition: fei.py:802
def _build_continuum_suppression(self, str particle_list, b2.Path path)
Definition: fei.py:832
def build_lists(self, path)
Definition: fei.py:749
def build_lists(self, path)
Definition: fei.py:567
def validation_histograms(self, path)
Definition: fei.py:599
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:669
def validation_histograms(self, path)
Definition: fei.py:677
def build_lists(self, path)
Definition: fei.py:925