Belle II Software  release-06-02-00
monitoring.py
1 #!/usr/bin/env python
2 
3 
10 
11 # @cond SUPPRESS_DOXYGEN
12 
13 """
14  Contains classes to read in the monitoring output
15  and some simple plotting routines.
16 
17  This is used by printReporting.py and latexReporting.py
18  to create summaries for a FEI training or application.
19 """
20 
21 try:
22  from generators import get_default_decayfile
23 except ModuleNotFoundError:
24  print("MonitoringBranchingFractions won't work.")
25 from basf2_mva_evaluation import plotting
26 import basf2_mva_util
27 import pickle
28 import copy
29 import math
30 import os
31 import numpy as np
32 import pdg
33 from ROOT import Belle2
34 import ROOT
35 from ROOT import gSystem
36 gSystem.Load('libanalysis.so')
38 
39 
40 def removeJPsiSlash(string):
41  """ Remove slashes in a string, which is not allowed for filenames. """
42  return string.replace('/', '')
43 
44 
45 def load_config():
46  """ Load the FEI configuration from the Summary.pickle file. """
47  if not os.path.isfile('Summary.pickle'):
48  raise RuntimeError("""Could not find Summary.pickle!
49  This file is automatically created by the FEI training.
50  But you can also create it yourself using:
51  pickle.dump((particles, configuration), open('Summary.pickle', 'wb'))""")
52  return pickle.load(open('Summary.pickle', 'rb'))
53 
54 
55 class Statistic:
56  """
57  This class provides the efficiency, purity and other quantities for a
58  given number of true signal candidates, signal candidates and background candidates
59  """
60 
61  def __init__(self, nTrueSig, nSig, nBg):
62  """
63  Create a new Statistic object
64  @param nTrueSig the number of true signal particles
65  @param nSig the number of reconstructed signal candidates
66  @param nBg the number of reconstructed background candidates
67  """
68 
69  self.nTrueSig = nTrueSig
70 
71  self.nSig = nSig
72 
73  self.nBg = nBg
74 
75  @property
76  def nTotal(self):
77  """ Returns total number of reconstructed candidates. """
78  return self.nSig + self.nBg
79 
80  @property
81  def purity(self):
82  """ Returns the purity of the reconstructed candidates. """
83  if self.nSig == 0:
84  return 0.0
85  if self.nTotal == 0:
86  return 0.0
87  return self.nSig / float(self.nTotal)
88 
89  @property
90  def efficiency(self):
91  """ Returns the efficiency of the reconstructed signal candidates with respect to the number of true signal particles. """
92  if self.nSig == 0:
93  return 0.0
94  if self.nTrueSig == 0:
95  return float('inf')
96  return self.nSig / float(self.nTrueSig)
97 
98  @property
99  def purityError(self):
100  """ Returns the uncertainty of the purity. """
101  if self.nTotal == 0:
102  return 0.0
103  return self.calcStandardDeviation(self.nSig, self.nTotal)
104 
105  @property
106  def efficiencyError(self):
107  """
108  Returns the uncertainty of the efficiency.
109  For an efficiency eps = self.nSig/self.nTrueSig, this function calculates the
110  standard deviation according to http://arxiv.org/abs/physics/0701199 .
111  """
112  if self.nTrueSig == 0:
113  return float('inf')
114  return self.calcStandardDeviation(self.nSig, self.nTrueSig)
115 
116  def calcStandardDeviation(self, k, n):
117  """ Helper method to calculate the standard deviation for efficiencies. """
118  k = float(k)
119  n = float(n)
120  variance = (k + 1) * (k + 2) / ((n + 2) * (n + 3)) - (k + 1) ** 2 / ((n + 2) ** 2)
121  if variance <= 0:
122  return 0.0
123  return math.sqrt(variance)
124 
125  def __str__(self):
126  """ Returns a string representation of a Statistic object. """
127  o = f"nTrueSig {self.nTrueSig} nSig {self.nSig} nBg {self.nBg}\n"
128  o += f"Efficiency {self.efficiency:.3f} ({self.efficiencyError:.3f})\n"
129  o += f"Purity {self.purity:.3f} ({self.purityError:.3f})\n"
130  return o
131 
132  def __add__(self, a):
133  """ Adds two Statistics objects and returns a new object. """
134  return Statistic(self.nTrueSig, self.nSig + a.nSig, self.nBg + a.nBg)
135 
136  def __radd__(self, a):
137  """
138  Returns a new Statistic object if the current one is added to zero.
139  Necessary to apply sum-function to Statistic objects.
140  """
141  if a != 0:
142  return NotImplemented
143  return Statistic(self.nTrueSig, self.nSig, self.nBg)
144 
145 
146 class MonitoringHist:
147  """
148  Reads all TH1F and TH2F from a ROOT file
149  and puts them into a more accessible format.
150  """
151 
152  def __init__(self, filename, dirname):
153  """
154  Reads histograms from the given file
155  @param filename the name of the ROOT file
156  """
157 
158  self.values = {}
159 
160  self.centers = {}
161 
162  self.nbins = {}
163 
164  self.valid = os.path.isfile(filename)
165 
166  if not self.valid:
167  return
168 
169  f = ROOT.TFile.Open(filename, 'read')
170  d = f.Get(Belle2.makeROOTCompatible(dirname))
171 
172  for key in d.GetListOfKeys():
173  name = Belle2.invertMakeROOTCompatible(key.GetName())
174  hist = key.ReadObj()
175  if not (isinstance(hist, ROOT.TH1D) or isinstance(hist, ROOT.TH1F) or
176  isinstance(hist, ROOT.TH2D) or isinstance(hist, ROOT.TH2F)):
177  continue
178  two_dimensional = isinstance(hist, ROOT.TH2D) or isinstance(hist, ROOT.TH2F)
179  if two_dimensional:
180  nbins = (hist.GetNbinsX(), hist.GetNbinsY())
181  self.centers[name] = np.array([[hist.GetXaxis().GetBinCenter(i) for i in range(nbins[0] + 2)],
182  [hist.GetYaxis().GetBinCenter(i) for i in range(nbins[1] + 2)]])
183  self.values[name] = np.array([[hist.GetBinContent(i, j) for i in range(nbins[0] + 2)] for j in range(nbins[1] + 2)])
184  self.nbins[name] = nbins
185  else:
186  nbins = hist.GetNbinsX()
187  self.centers[name] = np.array([hist.GetBinCenter(i) for i in range(nbins + 2)])
188  self.values[name] = np.array([hist.GetBinContent(i) for i in range(nbins + 2)])
189  self.nbins[name] = nbins
190 
191  def sum(self, name):
192  """
193  Calculates the sum of a given histogram (== sum of all entries)
194  @param name key of the histogram
195  """
196  if name not in self.centers:
197  return np.nan
198  return np.sum(self.values[name])
199 
200  def mean(self, name):
201  """
202  Calculates the mean of a given histogram
203  @param name key of the histogram
204  """
205  if name not in self.centers:
206  return np.nan
207  return np.average(self.centers[name], weights=self.values[name])
208 
209  def std(self, name):
210  """
211  Calculates the standard deviation of a given histogram
212  @param name key of the histogram
213  """
214  if name not in self.centers:
215  return np.nan
216  avg = np.average(self.centers[name], weights=self.values[name])
217  return np.sqrt(np.average((self.centers[name] - avg)**2, weights=self.values[name]))
218 
219  def min(self, name):
220  """
221  Calculates the minimum of a given histogram
222  @param name key of the histogram
223  """
224  if name not in self.centers:
225  return np.nan
226  nonzero = np.nonzero(self.values[name])[0]
227  if len(nonzero) == 0:
228  return np.nan
229  return self.centers[name][nonzero[0]]
230 
231  def max(self, name):
232  """
233  Calculates the maximum of a given histogram
234  @param name key of the histogram
235  """
236  if name not in self.centers:
237  return np.nan
238  nonzero = np.nonzero(self.values[name])[0]
239  if len(nonzero) == 0:
240  return np.nan
241  return self.centers[name][nonzero[-1]]
242 
243 
244 class MonitoringNTuple:
245  """
246  Reads the ntuple named variables from a ROOT file
247  """
248 
249  def __init__(self, filename, treenameprefix):
250  """
251  Reads ntuple from the given file
252  @param filename the name of the ROOT file
253  """
254 
255  self.valid = os.path.isfile(filename)
256  if not self.valid:
257  return
258 
259  self.f = ROOT.TFile.Open(filename, 'read')
260 
261  self.tree = self.f.Get(f'{treenameprefix} variables')
262 
263  self.filename = filename
264 
265 
266 class MonitoringModuleStatistics:
267  """
268  Reads the module statistics for a single particle from the outputted root file
269  and puts them into a more accessible format
270  """
271 
272  def __init__(self, particle):
273  """
274  Reads the module statistics from the file named Monitor_ModuleStatistics.root
275  @param particle the particle for which the statistics are read
276  """
277  root_file = ROOT.TFile.Open('Monitor_ModuleStatistics.root', 'read')
278  persistentTree = root_file.Get('persistent')
279  persistentTree.GetEntry(0)
280  # Clone() needed so we actually own the object (original dies when tfile is deleted)
281  stats = persistentTree.ProcessStatistics.Clone()
282 
283  # merge statistics from all persistent trees into 'stats'
284  numEntries = persistentTree.GetEntriesFast()
285  for i in range(1, numEntries):
286  persistentTree.GetEntry(i)
287  stats.merge(persistentTree.ProcessStatistics)
288 
289  # TODO .getTimeSum returns always 0 at the moment ?!
290  statistic = {m.getName(): m.getTimeSum(m.c_Event) / 1e9 for m in stats.getAll()}
291 
292 
293  self.channel_time = {}
294 
295  self.channel_time_per_module = {}
296  for channel in particle.channels:
297  if channel.label not in self.channel_time:
298  self.channel_time[channel.label] = 0.0
299  self.channel_time_per_module[channel.label] = {'ParticleCombiner': 0.0,
300  'BestCandidateSelection': 0.0,
301  'PListCutAndCopy': 0.0,
302  'VariablesToExtraInfo': 0.0,
303  'MCMatch': 0.0,
304  'ParticleSelector': 0.0,
305  'MVAExpert': 0.0,
306  'ParticleVertexFitter': 0.0,
307  'TagUniqueSignal': 0.0,
308  'VariablesToHistogram': 0.0,
309  'VariablesToNtuple': 0.0}
310  for key, time in statistic.items():
311  if(channel.decayString in key or channel.name in key):
312  self.channel_time[channel.label] += time
313  for k in self.channel_time_per_module[channel.label]:
314  if k in key:
315  self.channel_time_per_module[channel.label][k] += time
316 
317 
318  self.particle_time = 0
319  for key, time in statistic.items():
320  if particle.identifier in key:
321  self.particle_time += time
322 
323 
324 def MonitorCosBDLPlot(particle, filename):
325  """ Creates a CosBDL plot using ROOT. """
326  if not particle.final_ntuple.valid:
327  return
328  df = basf2_mva_util.tree2dict(particle.final_ntuple.tree,
329  ['extraInfo__bouniqueSignal__bc', 'cosThetaBetweenParticleAndNominalB',
330  'extraInfo__boSignalProbability__bc', particle.particle.mvaConfig.target],
331  ['unique', 'cosThetaBDl', 'probability', 'signal'])
332  for i, cut in enumerate([0.0, 0.01, 0.05, 0.1, 0.2, 0.5]):
333  p = plotting.VerboseDistribution(range_in_std=5.0)
334  common = (np.abs(df['cosThetaBDl']) < 10) & (df['probability'] >= cut)
335  p.add(df, 'cosThetaBDl', common & (df['signal'] == 1), label="Signal")
336  p.add(df, 'cosThetaBDl', common & (df['signal'] == 0), label="Background")
337  p.finish()
338  p.axis.set_title(f"Cosine of Theta between B and Dl system for signal probability >= {cut:.2f}")
339  p.axis.set_xlabel("CosThetaBDl")
340  p.save(f'{filename}_{i}.png')
341 
342 
343 def MonitorMbcPlot(particle, filename):
344  """ Creates a Mbc plot using ROOT. """
345  if not particle.final_ntuple.valid:
346  return
347  df = basf2_mva_util.tree2dict(particle.final_ntuple.tree,
348  ['extraInfo__bouniqueSignal__bc', 'Mbc',
349  'extraInfo__boSignalProbability__bc', particle.particle.mvaConfig.target],
350  ['unique', 'Mbc', 'probability', 'signal'])
351  for i, cut in enumerate([0.0, 0.01, 0.05, 0.1, 0.2, 0.5]):
352  p = plotting.VerboseDistribution(range_in_std=5.0)
353  common = (df['Mbc'] > 5.23) & (df['probability'] >= cut)
354  p.add(df, 'Mbc', common & (df['signal'] == 1), label="Signal")
355  p.add(df, 'Mbc', common & (df['signal'] == 0), label="Background")
356  p.finish()
357  p.axis.set_title(f"Beam constrained mass for signal probability >= {cut:.2f}")
358  p.axis.set_xlabel("Mbc")
359  p.save(f'{filename}_{i}.png')
360 
361 
362 def MonitorROCPlot(particle, filename):
363  """ Creates a ROC plot using ROOT. """
364  if not particle.final_ntuple.valid:
365  return
366  df = basf2_mva_util.tree2dict(particle.final_ntuple.tree,
367  ['extraInfo__bouniqueSignal__bc',
368  'extraInfo__boSignalProbability__bc', particle.particle.mvaConfig.target],
369  ['unique', 'probability', 'signal'])
371  p.add(df, 'probability', df['signal'] == 1, df['signal'] == 0, label='All')
372  p.finish()
373  p.save(filename + '.png')
374 
375 
376 def MonitorDiagPlot(particle, filename):
377  """ Creates a Diagonal plot using ROOT. """
378  if not particle.final_ntuple.valid:
379  return
380  df = basf2_mva_util.tree2dict(particle.final_ntuple.tree,
381  ['extraInfo__bouniqueSignal__bc',
382  'extraInfo__boSignalProbability__bc', particle.particle.mvaConfig.target],
383  ['unique', 'probability', 'signal'])
384  p = plotting.Diagonal()
385  p.add(df, 'probability', df['signal'] == 1, df['signal'] == 0)
386  p.finish()
387  p.save(filename + '.png')
388 
389 
390 def MonitoringMCCount(particle):
391  """
392  Reads the MC Counts for a given particle from the ROOT file mcParticlesCount.root
393  @param particle the particle for which the MC counts are read
394  @return dictionary with 'sum', 'std', 'avg', 'max', and 'min'
395  """
396  root_file = ROOT.TFile.Open('mcParticlesCount.root', 'read')
397 
398  key = f'NumberOfMCParticlesInEvent({abs(pdg.from_name(particle.name))})'
400  key = Belle2.makeROOTCompatible(key)
401  hist = root_file.Get(key)
402 
403  mc_counts = {'sum': 0, 'std': 0, 'avg': 0, 'min': 0, 'max': 0}
404  if hist:
405  mc_counts['sum'] = sum(hist.GetXaxis().GetBinCenter(bin + 1) * hist.GetBinContent(bin + 1)
406  for bin in range(hist.GetNbinsX()))
407  mc_counts['std'] = hist.GetStdDev()
408  mc_counts['avg'] = hist.GetMean()
409  mc_counts['max'] = hist.GetXaxis().GetBinCenter(hist.FindLastBinAbove(0.0))
410  mc_counts['min'] = hist.GetXaxis().GetBinCenter(hist.FindFirstBinAbove(0.0))
411  return mc_counts
412 
413 
414 class MonitoringBranchingFractions:
415  """ Class extracts the branching fractions of a decay channel from the DECAY.DEC file. """
416 
417  _shared = None
418 
419  def __init__(self):
420  """
421  Create a new MonitoringBranchingFraction object.
422  The extracted branching fractions are cached, hence creating more than one object does not do anything.
423  """
424  if MonitoringBranchingFractions._shared is None:
425  decay_file = get_default_decayfile()
426 
427  self.exclusive_branching_fractions = self.loadExclusiveBranchingFractions(decay_file)
428 
429  self.inclusive_branching_fractions = self.loadInclusiveBranchingFractions(self.exclusive_branching_fractions)
430  MonitoringBranchingFractions._shared = (self.exclusive_branching_fractions, self.inclusive_branching_fractions)
431  else:
432  self.exclusive_branching_fractions, self.inclusive_branching_fractions = MonitoringBranchingFractions._shared
433 
434  def getExclusive(self, particle):
435  """ Returns the exclusive (i.e. without the branching fractions of the daughters) branching fraction of a particle. """
436  return self.getBranchingFraction(particle, self.exclusive_branching_fractions)
437 
438  def getInclusive(self, particle):
439  """ Returns the inclusive (i.e. including all branching fractions of the daughters) branching fraction of a particle. """
440  return self.getBranchingFraction(particle, self.inclusive_branching_fractions)
441 
442  def getBranchingFraction(self, particle, branching_fractions):
443  """ Returns the branching fraction of a particle given a branching_fraction table. """
444  result = {c.label: 0.0 for c in particle.channels}
445  name = particle.name
446  channels = [tuple(sorted(d.split(':')[0] for d in channel.daughters)) for channel in particle.channels]
447  if name not in branching_fractions:
448  name = pdg.conjugate(name)
449  channels = [tuple(pdg.conjugate(d) for d in channel) for channel in channels]
450  if name not in branching_fractions:
451  return result
452  for c, key in zip(particle.channels, channels):
453  if key in branching_fractions[name]:
454  result[c.label] = branching_fractions[name][key]
455  return result
456 
457  def loadExclusiveBranchingFractions(self, filename):
458  """
459  Load branching fraction from MC decay-file.
460  """
461 
462  def isFloat(element):
463  """ Checks if element is a convertible to float"""
464  try:
465  float(element)
466  return True
467  except ValueError:
468  return False
469 
470  def isValidParticle(element):
471  """ Checks if element is a valid pdg name for a particle"""
472  try:
473  pdg.from_name(element)
474  return True
475  except LookupError:
476  return False
477 
478  branching_fractions = {'UNKOWN': {}}
479 
480  mother = 'UNKOWN'
481  with open(filename) as f:
482  for line in f:
483  fields = line.split(' ')
484  fields = [x for x in fields if x != '']
485  if len(fields) < 2 or fields[0][0] == '#':
486  continue
487  if fields[0] == 'Decay':
488  mother = fields[1].strip()
489  if not isValidParticle(mother):
490  mother = 'UNKOWN'
491  continue
492  if fields[0] == 'Enddecay':
493  mother = 'UNKOWN'
494  continue
495  if mother == 'UNKOWN':
496  continue
497  fields = fields[:-1]
498  if len(fields) < 1 or not isFloat(fields[0]):
499  continue
500  while len(fields) > 1:
501  if isValidParticle(fields[-1]):
502  break
503  fields = fields[:-1]
504  if len(fields) < 1 or not all(isValidParticle(p) for p in fields[1:]):
505  continue
506  neutrinoTag_list = ['nu_e', 'nu_mu', 'nu_tau', 'anti-nu_e', 'anti-nu_mu', 'anti-nu_tau']
507  daughters = tuple(sorted(p for p in fields[1:] if p not in neutrinoTag_list))
508  if mother not in branching_fractions:
509  branching_fractions[mother] = {}
510  if daughters not in branching_fractions[mother]:
511  branching_fractions[mother][daughters] = 0.0
512  branching_fractions[mother][daughters] += float(fields[0])
513 
514  del branching_fractions['UNKOWN']
515  return branching_fractions
516 
517  def loadInclusiveBranchingFractions(self, exclusive_branching_fractions):
518  """
519  Get covered branching fraction of a particle using a recursive algorithm
520  and the given exclusive branching_fractions (given as Hashable List)
521  @param particle identifier of the particle
522  @param branching_fractions
523  """
524  particles = set(exclusive_branching_fractions.keys())
525  particles.update({pdg.conjugate(p) for p in particles if p != pdg.conjugate(p)})
526  particles = sorted(particles, key=lambda x: pdg.get(x).Mass())
527  inclusive_branching_fractions = copy.deepcopy(exclusive_branching_fractions)
528 
529  for p in particles:
530  if p in inclusive_branching_fractions:
531  br = sum(inclusive_branching_fractions[p].values())
532  else:
533  br = sum(inclusive_branching_fractions[pdg.conjugate(p)].values())
534  for p_br in inclusive_branching_fractions.values():
535  for c in p_br:
536  for i in range(c.count(p)):
537  p_br[c] *= br
538  return inclusive_branching_fractions
539 
540 
541 class MonitoringParticle:
542  """
543  Monitoring object containing all the monitoring information
544  about a single particle
545  """
546 
547  def __init__(self, particle):
548  """
549  Read the monitoring information of the given particle
550  @param particle the particle for which the information is read
551  """
552 
553  self.particle = particle
554 
555  self.mc_count = MonitoringMCCount(particle)
556 
557  self.module_statistic = MonitoringModuleStatistics(particle)
558 
559  self.time_per_channel = self.module_statistic.channel_time
560 
561  self.time_per_channel_per_module = self.module_statistic.channel_time_per_module
562 
563  self.total_time = self.module_statistic.particle_time + sum(self.time_per_channel.values())
564 
565 
566  self.total_number_of_channels = len(self.particle.channels)
567 
568  self.reconstructed_number_of_channels = 0
569 
570 
571  self.branching_fractions = MonitoringBranchingFractions()
572 
573  self.exc_br_per_channel = self.branching_fractions.getExclusive(particle)
574 
575  self.inc_br_per_channel = self.branching_fractions.getInclusive(particle)
576 
577 
578  self.before_ranking = {}
579 
580  self.after_ranking = {}
581 
582  self.after_vertex = {}
583 
584  self.after_classifier = {}
585 
586  self.training_data = {}
587 
588  self.ignored_channels = {}
589 
590  for channel in self.particle.channels:
591  hist = MonitoringHist(f'Monitor_PreReconstruction_BeforeRanking.root', f'{channel.label}')
592  self.before_ranking[channel.label] = self.calculateStatistic(hist, channel.mvaConfig.target)
593  hist = MonitoringHist(f'Monitor_PreReconstruction_AfterRanking.root', f'{channel.label}')
594  self.after_ranking[channel.label] = self.calculateStatistic(hist, channel.mvaConfig.target)
595  hist = MonitoringHist(f'Monitor_PreReconstruction_AfterVertex.root', f'{channel.label}')
596  self.after_vertex[channel.label] = self.calculateStatistic(hist, channel.mvaConfig.target)
597  hist = MonitoringHist(f'Monitor_PostReconstruction_AfterMVA.root', f'{channel.label}')
598  self.after_classifier[channel.label] = self.calculateStatistic(hist, channel.mvaConfig.target)
599  if hist.valid and hist.sum(channel.mvaConfig.target) > 0:
600  self.reconstructed_number_of_channels += 1
601  self.ignored_channels[channel.label] = False
602  else:
603  self.ignored_channels[channel.label] = True
604  hist = MonitoringHist(f'Monitor_TrainingData.root', f'{channel.label}')
605  self.training_data[channel.label] = hist
606 
607  plist = removeJPsiSlash(particle.identifier)
608  hist = MonitoringHist(f'Monitor_PostReconstruction_BeforePostCut.root', f'{plist}')
609 
610  self.before_postcut = self.calculateStatistic(hist, self.particle.mvaConfig.target)
611  hist = MonitoringHist(f'Monitor_PostReconstruction_BeforeRanking.root', f'{plist}')
612 
613  self.before_ranking_postcut = self.calculateStatistic(hist, self.particle.mvaConfig.target)
614  hist = MonitoringHist(f'Monitor_PostReconstruction_AfterRanking.root', f'{plist}')
615 
616  self.after_ranking_postcut = self.calculateStatistic(hist, self.particle.mvaConfig.target)
617 
618  self.before_tag = self.calculateStatistic(hist, self.particle.mvaConfig.target)
619 
620  self.after_tag = self.calculateUniqueStatistic(hist)
621 
622  self.final_ntuple = MonitoringNTuple(f'Monitor_Final.root', f'{plist}')
623 
624  def calculateStatistic(self, hist, target):
625  """
626  Calculate Statistic object where all signal candidates are considered signal
627  """
628  nTrueSig = self.mc_count['sum']
629  if not hist.valid:
630  return Statistic(nTrueSig, 0, 0)
631  signal_bins = (hist.centers[target] > 0.5)
632  bckgrd_bins = ~signal_bins
633  nSig = hist.values[target][signal_bins].sum()
634  nBg = hist.values[target][bckgrd_bins].sum()
635  return Statistic(nTrueSig, nSig, nBg)
636 
637  def calculateUniqueStatistic(self, hist):
638  """
639  Calculate Static object where only unique signal candidates are considered signal
640  """
641  nTrueSig = self.mc_count['sum']
642  if not hist.valid:
643  return Statistic(nTrueSig, 0, 0)
644  signal_bins = hist.centers['extraInfo(uniqueSignal)'] > 0.5
645  bckgrd_bins = hist.centers['extraInfo(uniqueSignal)'] <= 0.5
646  nSig = hist.values['extraInfo(uniqueSignal)'][signal_bins].sum()
647  nBg = hist.values['extraInfo(uniqueSignal)'][bckgrd_bins].sum()
648  return Statistic(nTrueSig, nSig, nBg)
649 
650 # @endcond
def tree2dict(tree, tree_columns, dict_columns=None)
Global list of available variables.
Definition: Manager.h:98
static Manager & Instance()
get singleton instance.
Definition: Manager.cc:25
std::string makeROOTCompatible(std::string str)
Remove special characters that ROOT dislikes in branch names, e.g.
std::string invertMakeROOTCompatible(std::string str)
Invert makeROOTCompatible operation.
def conjugate(name)
Definition: pdg.py:110
def from_name(name)
Definition: pdg.py:62
def get(name)
Definition: pdg.py:47