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