12 The Full Event Interpretation Algorithm
15 - The algorithm will automatically reconstruct B mesons and calculate a signal probability for each candidate.
16 - It can be used for hadronic and semileptonic tagging.
17 - The algorithm has to be trained on MC, and can afterwards be applied on data.
18 - The training requires O(100) million MC events
19 - The weight files are stored in the Belle II Condition database
21 Read this file if you want to understand the technical details of the FEI.
23 The FEI follows a hierarchical approach.
25 (Stage -1: Write out information about the provided data sample)
26 Stage 0: Final State Particles (FSP)
27 Stage 1: pi0, J/Psi, Lambda0
29 Stage 3: D and Lambda_c mesons
34 Most stages consists of:
35 - Create Particle Candidates
38 - Apply a multivariate classification method
41 The FEI will reconstruct these 7 stages during the training phase,
42 since the stages depend on one another, you have to run basf2 multiple (7) times on the same data
43 to train all the necessary multivariate classifiers.
48from basf2
import B2INFO, B2WARNING, B2ERROR
50import modularAnalysis
as ma
71FeiState = collections.namedtuple(
'FeiState',
'path, stage, plists, fsplists, excludelists')
76 Contains the relevant information about the used training data.
77 Basically we write out the number of MC particles in the whole dataset.
78 This numbers we can use to calculate what fraction of candidates we have to write
79 out as TrainingData to get a reasonable amount of candidates to train on
80 (too few candidates will lead to heavy overtraining, too many won't fit into memory).
81 Secondly we can use this information for the generation of the monitoring pdfs,
82 where we calculate reconstruction efficiencies.
87 Create a new TrainingData object
88 @param particles list of config.Particle objects
89 @param outputPath path to the output directory
94 self.
filename = os.path.join(outputPath,
'mcParticlesCount.root')
98 Check if the relevant information is already available
100 return os.path.isfile(self.
filename)
104 Returns pybasf2.Path which counts the number of MCParticles in each event.
105 @param particles list of config.Particle objects
110 path = basf2.create_path()
111 module = basf2.register_module(
'VariablesToHistogram')
112 module.set_name(
"VariablesToHistogram_MCCount")
113 module.param(
'variables', [(f
'NumberOfMCParticlesInEvent({pdg})', 100, -0.5, 99.5)
for pdg
in pdgs])
114 module.param(
'fileName', self.
filename)
115 module.param(
'ignoreCommandLineOverride',
True)
116 path.add_module(module)
121 Read out the number of MC particles from the file created by reconstruct
126 root_file = ROOT.TFile.Open(self.
filename,
'read')
129 for key
in root_file.GetListOfKeys():
130 variable = ROOT.Belle2.MakeROOTCompatible.invertMakeROOTCompatible(key.GetName())
131 pdg = abs(int(variable[len(
'NumberOfMCParticlesInEvent('):-len(
")")]))
134 mc_counts[pdg][
'sum'] = sum(hist.GetXaxis().GetBinCenter(bin + 1) * hist.GetBinContent(bin + 1)
135 for bin
in range(hist.GetNbinsX()))
136 mc_counts[pdg][
'std'] = hist.GetStdDev()
137 mc_counts[pdg][
'avg'] = hist.GetMean()
138 mc_counts[pdg][
'max'] = hist.GetXaxis().GetBinCenter(hist.FindLastBinAbove(0.0))
139 mc_counts[pdg][
'min'] = hist.GetXaxis().GetBinCenter(hist.FindFirstBinAbove(0.0))
142 mc_counts[0][
'sum'] = hist.GetEntries()
149 Steers the loading of FSP particles.
150 This does NOT include RootInput, Geometry or anything required before loading FSPs,
151 the user has to add this himself (because it depends on the MC campaign and if you want
152 to use Belle or Belle II).
157 Create a new FSPLoader object
158 @param particles list of config.Particle objects
159 @param config config.FeiConfiguration object
168 Returns a list of FSP particle lists which are used in the FEI.
169 This is used to create the RootOutput module.
171 fsps = [
'K+:FSP',
'pi+:FSP',
'e+:FSP',
'mu+:FSP',
'p+:FSP',
'gamma:FSP',
'K_S0:V0',
'Lambda0:V0',
'K_L0:FSP',
'gamma:V0']
178 Returns pybasf2.Path which loads the FSP Particles
180 path = basf2.create_path()
183 ma.fillParticleLists([(
'K+:FSP',
''), (
'pi+:FSP',
''), (
'e+:FSP',
''),
184 (
'mu+:FSP',
''), (
'p+:FSP',
'')], writeOut=
True, path=path)
185 for outputList, inputList
in [(
'gamma:FSP',
'gamma:mdst'), (
'K_S0:V0',
'K_S0:mdst'),
186 (
'Lambda0:V0',
'Lambda0:mdst'), (
'K_L0:FSP',
'K_L0:mdst'),
187 (
'pi0:FSP',
'pi0:mdst'), (
'gamma:V0',
'gamma:v0mdst')]:
188 ma.copyParticles(outputList, inputList, writeOut=
True, path=path)
190 ma.fillParticleLists([(
'K+:FSP',
''), (
'pi+:FSP',
''), (
'e+:FSP',
''),
191 (
'mu+:FSP',
''), (
'gamma:FSP',
''),
192 (
'p+:FSP',
''), (
'K_L0:FSP',
'')], writeOut=
True, path=path)
193 ma.fillParticleList(
'K_S0:V0 -> pi+ pi-',
'', writeOut=
True, path=path)
194 ma.fillParticleList(
'Lambda0:V0 -> p+ pi-',
'', writeOut=
True, path=path)
195 ma.fillConvertedPhotonsList(
'gamma:V0 -> e+ e-',
'', writeOut=
True, path=path)
198 names = [
'e+',
'K+',
'pi+',
'mu+',
'gamma',
'K_S0',
'p+',
'K_L0',
'Lambda0',
'pi0']
199 filename = os.path.join(self.
config.monitoring_path,
'Monitor_FSPLoader.root')
201 variables = [(f
'NumberOfMCParticlesInEvent({pdg})', 100, -0.5, 99.5)
for pdg
in pdgs]
202 ma.variablesToHistogram(
'', variables=variables, filename=filename, ignoreCommandLineOverride=
True, path=path)
208 Steers the creation of the training data.
209 The training data is used to train a multivariate classifier for each channel.
210 The training of the FEI at its core is just generating this training data for each channel.
211 After we created the training data for a stage, we have to train the classifiers (see Teacher class further down).
215 mc_counts: typing.Mapping[int, typing.Mapping[str, float]]):
217 Create a new TrainingData object
218 @param particles list of config.Particle objects
219 @param config config.FeiConfiguration object
220 @param mc_counts containing number of MC Particles
231 Returns pybasf2.Path which creates the training data for the given particles
234 path = basf2.create_path()
239 print(f
"FEI-core: TrainingData: nSignal for {particle.name}: {nSignal}")
248 for channel
in particle.channels:
249 weightfile = f
'{channel.label}.xml'
250 if basf2_mva.available(weightfile):
251 B2INFO(f
"FEI-core: Skipping preparing Training Data for {weightfile}, already available")
253 filename =
'training_input.root'
256 nBackground = self.
mc_counts[0][
'sum'] * channel.preCutConfig.bestCandidateCut
257 inverseSamplingRates = {}
260 if nBackground > Teacher.MaximumNumberOfMVASamples
and not channel.preCutConfig.noBackgroundSampling:
261 inverseSamplingRates[0] = max(
262 1, int((int(nBackground / Teacher.MaximumNumberOfMVASamples) + 1) * channel.preCutConfig.bkgSamplingFactor))
263 elif channel.preCutConfig.bkgSamplingFactor > 1:
264 inverseSamplingRates[0] = int(channel.preCutConfig.bkgSamplingFactor)
266 if nSignal > Teacher.MaximumNumberOfMVASamples
and not channel.preCutConfig.noSignalSampling:
267 inverseSamplingRates[1] = int(nSignal / Teacher.MaximumNumberOfMVASamples) + 1
269 spectators = [channel.mvaConfig.target] + list(channel.mvaConfig.spectators.keys())
270 if channel.mvaConfig.sPlotVariable
is not None:
271 spectators.append(channel.mvaConfig.sPlotVariable)
274 hist_variables = [
'mcErrors',
'mcParticleStatus'] + channel.mvaConfig.variables + spectators
275 hist_variables_2d = [(x, channel.mvaConfig.target)
276 for x
in channel.mvaConfig.variables + spectators
if x
is not channel.mvaConfig.target]
277 hist_filename = os.path.join(self.
config.monitoring_path,
'Monitor_TrainingData.root')
278 ma.variablesToHistogram(channel.name, variables=config.variables2binnings(hist_variables),
279 variables_2d=config.variables2binnings_2d(hist_variables_2d),
280 filename=hist_filename,
281 ignoreCommandLineOverride=
True,
282 directory=config.removeJPsiSlash(f
'{channel.label}'), path=path)
284 teacher = basf2.register_module(
'VariablesToNtuple')
285 teacher.set_name(f
'VariablesToNtuple_{channel.name}')
286 teacher.param(
'fileName', filename)
287 teacher.param(
'treeName', ROOT.Belle2.MakeROOTCompatible.makeROOTCompatible(f
'{channel.label} variables'))
288 teacher.param(
'variables', channel.mvaConfig.variables + spectators)
289 teacher.param(
'particleList', channel.name)
290 teacher.param(
'sampling', (channel.mvaConfig.target, inverseSamplingRates))
291 teacher.param(
'ignoreCommandLineOverride',
True)
292 path.add_module(teacher)
298 Steers the reconstruction phase before the mva method was applied
300 - The ParticleCombination (for each particle and channel we create candidates using
301 the daughter candidates from the previous stages)
303 - Vertex Fitting (this is the slowest part of the whole FEI, KFit is used by default,
304 but you can use fastFit as a drop-in replacement https://github.com/thomaskeck/FastFit/,
305 this will speed up the whole FEI by a factor 2-3)
310 Create a new PreReconstruction object
311 @param particles list of config.Particle objects
312 @param config config.FeiConfiguration object
321 Returns pybasf2.Path which reconstructs the particles and does the vertex fitting if necessary
323 path = basf2.create_path()
326 for channel
in particle.channels:
329 channel.daughters[0].split(
':')[0]) ==
pdg.from_name(particle.name)):
330 ma.cutAndCopyList(channel.name, channel.daughters[0], channel.preCutConfig.userCut, writeOut=
True, path=path)
331 v2EI = basf2.register_module(
'VariablesToExtraInfo')
332 v2EI.set_name(f
'VariablesToExtraInfo_{channel.name}')
333 v2EI.param(
'particleList', channel.name)
334 v2EI.param(
'variables', {f
'constant({channel.decayModeID})':
'decayModeID'})
336 v2EI.set_log_level(basf2.logging.log_level.ERROR)
337 path.add_module(v2EI)
339 ma.reconstructDecay(channel.decayString, channel.preCutConfig.userCut, channel.decayModeID,
340 writeOut=
True, path=path)
344 if channel.extraPathSpec
is not None:
345 spec = channel.extraPathSpec
346 mod = importlib.import_module(spec[
"module"])
347 registerFunc = getattr(mod, spec[
"function"])
348 registerFunc(path, channel.name, *spec[
"args"], **spec[
"kwargs"])
351 if "tag" in (channel.name).lower():
352 ma.matchTagTruth(channel.name, path=path)
354 ma.matchMCTruth(channel.name, path=path)
355 bc_variable = channel.preCutConfig.bestCandidateVariable
356 if self.
config.monitor ==
'simple':
357 hist_variables = [channel.mvaConfig.target,
'extraInfo(decayModeID)']
358 hist_variables_2d = [(channel.mvaConfig.target,
'extraInfo(decayModeID)')]
360 hist_variables = [bc_variable,
'mcErrors',
'mcParticleStatus',
361 channel.mvaConfig.target] + list(channel.mvaConfig.spectators.keys())
362 hist_variables_2d = [(bc_variable, channel.mvaConfig.target),
363 (bc_variable,
'mcErrors'),
364 (bc_variable,
'mcParticleStatus')]
365 for specVar
in channel.mvaConfig.spectators:
366 hist_variables_2d.append((bc_variable, specVar))
367 hist_variables_2d.append((channel.mvaConfig.target, specVar))
368 filename = os.path.join(self.
config.monitoring_path,
'Monitor_PreReconstruction_BeforeRanking.root')
369 ma.variablesToHistogram(
371 variables=config.variables2binnings(hist_variables),
372 variables_2d=config.variables2binnings_2d(hist_variables_2d),
374 ignoreCommandLineOverride=
True,
375 directory=f
'{channel.label}',
378 if channel.preCutConfig.bestCandidateMode ==
'lowest':
379 ma.rankByLowest(channel.name,
380 channel.preCutConfig.bestCandidateVariable,
381 channel.preCutConfig.bestCandidateCut,
384 elif channel.preCutConfig.bestCandidateMode ==
'highest':
385 ma.rankByHighest(channel.name,
386 channel.preCutConfig.bestCandidateVariable,
387 channel.preCutConfig.bestCandidateCut,
391 raise RuntimeError(f
'Unknown bestCandidateMode {repr(channel.preCutConfig.bestCandidateMode)}')
393 if 'gamma' in channel.decayString
and channel.pi0veto:
394 ma.buildRestOfEvent(channel.name, path=path)
395 Ddaughter_roe_path = basf2.Path()
396 deadEndPath = basf2.Path()
397 ma.signalSideParticleFilter(channel.name,
'', Ddaughter_roe_path, deadEndPath)
398 ma.fillParticleList(
'gamma:roe',
'isInRestOfEvent == 1', path=Ddaughter_roe_path)
400 matches = list(re.finditer(
'gamma', channel.decayString))
402 for igamma
in range(len(matches)):
403 start, end = matches[igamma-1].span()
404 tempString = f
'{channel.decayString[:start]}^gamma{channel.decayString[end:]}'
405 ma.fillSignalSideParticleList(f
'gamma:sig_{igamma}', tempString, path=Ddaughter_roe_path)
406 ma.reconstructDecay(f
'pi0:veto_{igamma} -> gamma:sig_{igamma} gamma:roe',
'', path=Ddaughter_roe_path)
407 pi0lists.append(f
'pi0:veto_{igamma}')
408 ma.copyLists(
'pi0:veto', pi0lists, writeOut=
False, path=Ddaughter_roe_path)
409 ma.rankByLowest(
'pi0:veto',
'abs(dM)', 1, path=Ddaughter_roe_path)
410 ma.matchMCTruth(
'pi0:veto', path=Ddaughter_roe_path)
411 ma.variableToSignalSideExtraInfo(
414 'InvM':
'pi0vetoMass',
415 'formula((daughter(0,E)-daughter(1,E))/(daughter(0,E)+daughter(1,E)))':
'pi0vetoEneAsy',
416 'cosHelicityAngleMomentum':
'pi0vetoCosHelMom',
418 path=Ddaughter_roe_path
420 path.for_each(
'RestOfEvent',
'RestOfEvents', Ddaughter_roe_path)
423 filename = os.path.join(self.
config.monitoring_path,
'Monitor_PreReconstruction_AfterRanking.root')
424 if self.
config.monitor !=
'simple':
425 hist_variables += [
'extraInfo(preCut_rank)']
426 hist_variables_2d += [(
'extraInfo(preCut_rank)', channel.mvaConfig.target),
427 (
'extraInfo(preCut_rank)',
'mcErrors'),
428 (
'extraInfo(preCut_rank)',
'mcParticleStatus')]
429 for specVar
in channel.mvaConfig.spectators:
430 hist_variables_2d.append((
'extraInfo(preCut_rank)', specVar))
431 ma.variablesToHistogram(
433 variables=config.variables2binnings(hist_variables),
434 variables_2d=config.variables2binnings_2d(hist_variables_2d),
436 ignoreCommandLineOverride=
True,
437 directory=f
'{channel.label}',
441 elif self.
config.training:
442 if "tag" in (channel.name).lower():
443 ma.matchTagTruth(channel.name, path=path)
445 ma.matchMCTruth(channel.name, path=path)
448 pvfit = basf2.register_module(
'ParticleVertexFitter')
449 pvfit.set_name(f
'ParticleVertexFitter_{channel.name}')
450 pvfit.param(
'listName', channel.name)
451 pvfit.param(
'confidenceLevel', channel.preCutConfig.vertexCut)
452 pvfit.param(
'vertexFitter',
'KFit')
453 pvfit.param(
'fitType',
'vertex')
454 pvfit.set_log_level(basf2.logging.log_level.ERROR)
455 path.add_module(pvfit)
456 elif re.findall(
r"[\w']+", channel.decayString).count(
'pi0') > 1
and particle.name !=
'pi0':
457 basf2.B2INFO(f
"Ignoring vertex fit for {channel.name} because multiple pi0 are not supported yet.")
458 elif len(channel.daughters) > 1:
459 pvfit = basf2.register_module(
'ParticleVertexFitter')
460 pvfit.set_name(f
'ParticleVertexFitter_{channel.name}')
461 pvfit.param(
'listName', channel.name)
462 pvfit.param(
'confidenceLevel', channel.preCutConfig.vertexCut)
463 pvfit.param(
'vertexFitter',
'KFit')
464 if particle.name
in [
'pi0']:
465 pvfit.param(
'fitType',
'mass')
467 pvfit.param(
'fitType',
'vertex')
468 pvfit.set_log_level(basf2.logging.log_level.ERROR)
469 path.add_module(pvfit)
472 if self.
config.monitor ==
'simple':
473 hist_variables = [channel.mvaConfig.target,
'extraInfo(decayModeID)']
474 hist_variables_2d = [(channel.mvaConfig.target,
'extraInfo(decayModeID)')]
476 hist_variables = [
'chiProb',
'mcErrors',
'mcParticleStatus',
477 channel.mvaConfig.target] + list(channel.mvaConfig.spectators.keys())
478 hist_variables_2d = [(
'chiProb', channel.mvaConfig.target),
479 (
'chiProb',
'mcErrors'),
480 (
'chiProb',
'mcParticleStatus')]
481 for specVar
in channel.mvaConfig.spectators:
482 hist_variables_2d.append((
'chiProb', specVar))
483 hist_variables_2d.append((channel.mvaConfig.target, specVar))
484 filename = os.path.join(self.
config.monitoring_path,
'Monitor_PreReconstruction_AfterVertex.root')
485 ma.variablesToHistogram(
487 variables=config.variables2binnings(hist_variables),
488 variables_2d=config.variables2binnings_2d(hist_variables_2d),
490 ignoreCommandLineOverride=
True,
491 directory=f
'{channel.label}',
499 Steers the reconstruction phase after the mva method was applied
501 - The application of the mva method itself.
502 - Copying all channel lists in a common one for each particle defined in particles
503 - Tag unique signal candidates, to avoid double counting of channels with overlap
508 Create a new PostReconstruction object
509 @param particles list of config.Particle objects
510 @param config config.FeiConfiguration object
519 Returns all channels for which the weightfile is missing
523 for channel
in particle.channels:
525 weightfile = f
'{channel.label}.xml'
526 if not basf2_mva.available(weightfile):
527 missing += [channel.label]
532 Check if the relevant information is already available
538 Returns pybasf2.Path which reconstructs the particles and does the vertex fitting if necessary
541 path = basf2.create_path()
544 for channel
in particle.channels:
545 expert = basf2.register_module(
'MVAExpert')
546 expert.set_name(f
'MVAExpert_{channel.name}')
548 expert.param(
'identifier', f
'{channel.label}.xml')
550 expert.param(
'identifier', f
'{self.config.prefix}_{channel.label}')
551 expert.param(
'extraInfoName',
'SignalProbability')
552 expert.param(
'listNames', [channel.name])
554 expert.set_log_level(basf2.logging.log_level.ERROR)
555 path.add_module(expert)
558 if self.
config.monitor ==
'simple':
559 hist_variables = [channel.mvaConfig.target,
'extraInfo(decayModeID)']
560 hist_variables_2d = [(channel.mvaConfig.target,
'extraInfo(decayModeID)')]
562 hist_variables = [
'mcErrors',
564 'extraInfo(SignalProbability)',
565 channel.mvaConfig.target,
566 'extraInfo(decayModeID)'] + list(channel.mvaConfig.spectators.keys())
567 hist_variables_2d = [(
'extraInfo(SignalProbability)', channel.mvaConfig.target),
568 (
'extraInfo(SignalProbability)',
'mcErrors'),
569 (
'extraInfo(SignalProbability)',
'mcParticleStatus'),
570 (
'extraInfo(decayModeID)', channel.mvaConfig.target),
571 (
'extraInfo(decayModeID)',
'mcErrors'),
572 (
'extraInfo(decayModeID)',
'mcParticleStatus')]
573 for specVar
in channel.mvaConfig.spectators:
574 hist_variables_2d.append((
'extraInfo(SignalProbability)', specVar))
575 hist_variables_2d.append((
'extraInfo(decayModeID)', specVar))
576 hist_variables_2d.append((channel.mvaConfig.target, specVar))
577 filename = os.path.join(self.
config.monitoring_path,
'Monitor_PostReconstruction_AfterMVA.root')
578 ma.variablesToHistogram(
580 variables=config.variables2binnings(hist_variables),
581 variables_2d=config.variables2binnings_2d(hist_variables_2d),
583 ignoreCommandLineOverride=
True,
584 directory=f
'{channel.label}',
588 if particle.postCutConfig.value > 0.0:
589 cutstring = f
'{particle.postCutConfig.value} < extraInfo(SignalProbability)'
591 ma.mergeListsWithBestDuplicate(particle.identifier, [c.name
for c
in particle.channels],
592 variable=
'particleSource', writeOut=
True, path=path)
595 if self.
config.monitor ==
'simple':
596 hist_variables = [particle.mvaConfig.target,
'extraInfo(decayModeID)']
597 hist_variables_2d = [(particle.mvaConfig.target,
'extraInfo(decayModeID)')]
599 hist_variables = [
'mcErrors',
601 'extraInfo(SignalProbability)',
602 particle.mvaConfig.target,
603 'extraInfo(decayModeID)'] + list(particle.mvaConfig.spectators.keys())
604 hist_variables_2d = [(
'extraInfo(decayModeID)', particle.mvaConfig.target),
605 (
'extraInfo(decayModeID)',
'mcErrors'),
606 (
'extraInfo(decayModeID)',
'mcParticleStatus')]
607 for specVar
in particle.mvaConfig.spectators:
608 hist_variables_2d.append((
'extraInfo(SignalProbability)', specVar))
609 hist_variables_2d.append((
'extraInfo(decayModeID)', specVar))
610 hist_variables_2d.append((particle.mvaConfig.target, specVar))
611 filename = os.path.join(self.
config.monitoring_path,
'Monitor_PostReconstruction_BeforePostCut.root')
612 ma.variablesToHistogram(
614 variables=config.variables2binnings(hist_variables),
615 variables_2d=config.variables2binnings_2d(hist_variables_2d),
617 ignoreCommandLineOverride=
True,
618 directory=config.removeJPsiSlash(f
'{particle.identifier}'),
621 ma.applyCuts(particle.identifier, cutstring, path=path)
624 filename = os.path.join(self.
config.monitoring_path,
'Monitor_PostReconstruction_BeforeRanking.root')
625 ma.variablesToHistogram(
627 variables=config.variables2binnings(hist_variables),
628 variables_2d=config.variables2binnings_2d(hist_variables_2d),
630 ignoreCommandLineOverride=
True,
631 directory=config.removeJPsiSlash(f
'{particle.identifier}'),
634 ma.rankByHighest(particle.identifier,
'extraInfo(SignalProbability)',
635 particle.postCutConfig.bestCandidateCut,
'postCut_rank', path=path)
637 uniqueSignal = basf2.register_module(
'TagUniqueSignal')
638 uniqueSignal.param(
'particleList', particle.identifier)
639 uniqueSignal.param(
'target', particle.mvaConfig.target)
640 uniqueSignal.param(
'extraInfoName',
'uniqueSignal')
641 uniqueSignal.set_name(f
'TagUniqueSignal_{particle.identifier}')
643 uniqueSignal.set_log_level(basf2.logging.log_level.ERROR)
644 path.add_module(uniqueSignal)
647 if self.
config.monitor !=
'simple':
648 hist_variables += [
'extraInfo(postCut_rank)']
649 hist_variables_2d += [(
'extraInfo(decayModeID)',
'extraInfo(postCut_rank)'),
650 (particle.mvaConfig.target,
'extraInfo(postCut_rank)'),
651 (
'mcErrors',
'extraInfo(postCut_rank)'),
652 (
'mcParticleStatus',
'extraInfo(postCut_rank)')]
653 for specVar
in particle.mvaConfig.spectators:
654 hist_variables_2d.append((
'extraInfo(postCut_rank)', specVar))
655 filename = os.path.join(self.
config.monitoring_path,
'Monitor_PostReconstruction_AfterRanking.root')
656 ma.variablesToHistogram(
658 variables=config.variables2binnings(hist_variables),
659 variables_2d=config.variables2binnings_2d(hist_variables_2d),
661 ignoreCommandLineOverride=
True,
662 directory=config.removeJPsiSlash(f
'{particle.identifier}'),
665 filename = os.path.join(self.
config.monitoring_path,
'Monitor_Final.root')
666 if self.
config.monitor ==
'simple':
667 hist_variables = [
'extraInfo(uniqueSignal)',
'extraInfo(decayModeID)']
668 hist_variables_2d = [(
'extraInfo(uniqueSignal)',
'extraInfo(decayModeID)')]
669 ma.variablesToHistogram(
671 variables=config.variables2binnings(hist_variables),
672 variables_2d=config.variables2binnings_2d(hist_variables_2d),
674 ignoreCommandLineOverride=
True,
675 directory=config.removeJPsiSlash(f
'{particle.identifier}'),
678 variables = [
'extraInfo(SignalProbability)',
'mcErrors',
'mcParticleStatus', particle.mvaConfig.target,
679 'extraInfo(uniqueSignal)',
'extraInfo(decayModeID)'] + list(particle.mvaConfig.spectators.keys())
681 ma.variablesToNtuple(
684 treename=ROOT.Belle2.MakeROOTCompatible.makeROOTCompatible(
685 config.removeJPsiSlash(f
'{particle.identifier} variables')),
687 ignoreCommandLineOverride=
True,
694 Performs all necessary trainings for all training data files which are
695 available but where there is no weight file available yet.
696 This class is usually used by the do_trainings function below, to perform the necessary trainings after each stage.
697 The trainings are run in parallel using multi-threading of python.
698 Each training is done by a subprocess call, the training command (passed by config.externTeacher) can be either
699 * basf2_mva_teacher, the training will be done directly on the machine
700 * externClustTeacher, the training will be submitted to the batch system of KEKCC
704 MaximumNumberOfMVASamples = int(1e7)
707 MinimumNumberOfMVASamples = int(5e2)
711 Create a new Teacher object
712 @param particles list of config.Particle objects
713 @param config config.FeiConfiguration object
723 Create a fake weight file using the trivial method, it will always return 0.0
724 @param channel for which we create a fake weight file
727 <?xml version="1.0" encoding="utf-8"?>
728 <method>Trivial</method>
729 <weightfile>{channel}.xml</weightfile>
730 <treename>tree</treename>
731 <target_variable>isSignal</target_variable>
732 <weight_variable>__weight__</weight_variable>
733 <signal_class>1</signal_class>
734 <max_events>0</max_events>
735 <number_feature_variables>1</number_feature_variables>
736 <variable0>M</variable0>
737 <number_spectator_variables>0</number_spectator_variables>
738 <number_data_files>1</number_data_files>
739 <datafile0>train.root</datafile0>
740 <Trivial_version>1</Trivial_version>
741 <Trivial_output>0</Trivial_output>
742 <signal_fraction>0.066082567</signal_fraction>
744 with open(f
'{channel}.xml',
"w")
as f:
750 Checks if the provided filename is a fake-weight file or not
751 @param filename the filename of the weight file
754 return '<method>Trivial</method>' in open(filename).readlines()[2]
755 except BaseException:
761 Upload the weight file into the condition database
762 @param channel whose weight file is uploaded
764 disk = f
'{channel}.xml'
765 dbase = f
'{self.config.prefix}_{channel}'
766 basf2_mva.upload(disk, dbase)
767 print(f
"FEI-core: Uploading {dbase} to localdb")
772 Do all trainings for which we find training data
778 ROOT.PyConfig.StartGuiThread =
False
781 all_stage_particles = get_stages_from_particles(self.
particles)
782 if self.
config.cache
is None:
783 stagesToTrain = range(1, len(all_stage_particles)+1)
785 stagesToTrain = [self.
config.cache]
787 filename =
'training_input.root'
788 if os.path.isfile(filename):
789 f = ROOT.TFile.Open(filename,
'read')
791 B2WARNING(f
'Training of MVC failed: {filename}. ROOT file corrupt. No weight files will be provided.')
792 elif len([k.GetName()
for k
in f.GetListOfKeys()]) == 0:
794 f
'Training of MVC failed: {filename}. ROOT file has no trees. No weight files will be provided.')
796 for istage
in stagesToTrain:
797 for particle
in all_stage_particles[istage-1]:
798 for channel
in particle.channels:
799 weightfile = f
'{channel.label}.xml'
800 if basf2_mva.available(weightfile):
801 B2INFO(f
"FEI-core: Skipping {weightfile}, already available")
804 treeName = ROOT.Belle2.MakeROOTCompatible.makeROOTCompatible(f
'{channel.label} variables')
805 keys = [m
for m
in f.GetListOfKeys()
if treeName
in m.GetName()]
807 B2WARNING(
"Training of MVC failed. "
808 f
"Couldn't find tree for channel {channel}. Ignoring channel.")
811 B2WARNING(f
"Found more than one tree for channel {channel}. Taking first tree from: {keys}")
812 tree = keys[0].ReadObj()
813 total_entries = tree.GetEntries()
814 nSig = tree.GetEntries(f
'{channel.mvaConfig.target}==1.0')
815 nBg = tree.GetEntries(f
'{channel.mvaConfig.target}==0.0')
817 f
'FEI-core: Number of events for channel: {channel.label}, '
818 f
'Total: {total_entries}, Signal: {nSig}, Background: {nBg}')
819 if nSig < Teacher.MinimumNumberOfMVASamples:
820 B2WARNING(
"Training of MVC failed. "
821 f
"Tree contains too few signal events {nSig}. Ignoring channel {channel}.")
823 self.
upload(channel.label)
825 if nBg < Teacher.MinimumNumberOfMVASamples:
826 B2WARNING(
"Training of MVC failed. "
827 f
"Tree contains too few bckgrd events {nBg}. Ignoring channel {channel}.")
829 self.
upload(channel.label)
831 variable_str =
"' '".join(channel.mvaConfig.variables)
833 spectators = list(channel.mvaConfig.spectators.keys())
834 if channel.mvaConfig.sPlotVariable
is not None:
835 spectators.append(channel.mvaConfig.sPlotVariable)
836 spectators_str =
"' '".join(spectators)
838 treeName = ROOT.Belle2.MakeROOTCompatible.makeROOTCompatible(f
'{channel.label} variables')
839 command = (f
"{self.config.externTeacher}"
840 f
" --method '{channel.mvaConfig.method}'"
841 f
" --target_variable '{channel.mvaConfig.target}'"
842 f
" --treename '{treeName}'"
843 f
" --datafile 'training_input.root'"
845 f
" --variables '{variable_str}'"
846 f
" --identifier '{weightfile}'")
847 if len(spectators) > 0:
848 command += f
" --spectators '{spectators_str}'"
849 command += f
" {channel.mvaConfig.config} > '{channel.label}'.log 2>&1"
850 B2INFO(f
"Used following command to invoke teacher: \n {command}")
851 job_list.append((channel.label, command))
854 if len(job_list) > 0:
855 p = multiprocessing.Pool(
None, maxtasksperchild=1)
856 func = functools.partial(subprocess.call, shell=
True)
857 p.map(func, [c
for _, c
in job_list])
861 for name, _
in job_list:
862 if not basf2_mva.available(f
'{name}.xml'):
863 B2WARNING(
"Training of MVC failed. For unknown reasons, check the logfile", f
'{name}.log')
865 weightfiles.append(self.
upload(name))
869def convert_legacy_training(particles: typing.Sequence[
config.Particle], configuration: config.FeiConfiguration):
871 Convert an old FEI training into the new format.
872 The old format used hashes for the weight files, the hashes can be converted to the new naming scheme
873 using the Summary.pickle file outputted by the FEIv3. This file must be passes by the parameter configuration.legacy.
874 @param particles list of config.Particle objects
875 @param config config.FeiConfiguration object
877 summary = pickle.load(open(configuration.legacy,
'rb'))
878 channel2lists = {k: v[2]
for k, v
in summary[
'channel2lists'].items()}
880 teacher =
Teacher(particles, configuration)
882 for particle
in particles:
883 for channel
in particle.channels:
884 new_weightfile = f
'{configuration.prefix}_{channel.label}'
885 old_weightfile = f
'{configuration.prefix}_{channel2lists[channel.label.replace("Jpsi", "J/psi")]}'
886 if not basf2_mva.available(new_weightfile):
887 if old_weightfile
is None or not basf2_mva.available(old_weightfile):
888 Teacher.create_fake_weightfile(channel.label)
889 teacher.upload(channel.label)
891 basf2_mva.download(old_weightfile, f
'{channel.label}.xml')
892 teacher.upload(channel.label)
895def get_stages_from_particles(particles: typing.Sequence[typing.Union[
config.Particle, str]]):
897 Returns the hierarchical structure of the FEI.
898 Each stage depends on the particles in the previous stage.
899 The final stage is empty (meaning everything is done, and the training is finished at this point).
900 @param particles list of config.Particle or string objects
903 return p.split(
":")[0]
if isinstance(p, str)
else p.name
906 return (p.split(
":")[1]
if isinstance(p, str)
else p.label).lower()
909 [p
for p
in particles
if get_pname(p)
in [
'e+',
'K+',
'pi+',
'mu+',
'gamma',
'p+',
'K_L0']],
910 [p
for p
in particles
if get_pname(p)
in [
'pi0',
'J/psi',
'Lambda0']],
911 [p
for p
in particles
if get_pname(p)
in [
'K_S0',
'Sigma+',
'Sigma0',
'Xi0',
'Xi-']],
912 [p
for p
in particles
if get_pname(p)
in [
'D+',
'D0',
'D_s+',
'Lambda_c+',
'Omega-']
and 'tag' not in get_plabel(p)],
913 [p
for p
in particles
if get_pname(p)
in [
'D*+',
'D*0',
'D_s*+',
914 'Sigma_c+',
'Sigma_c0',
'Sigma_c++',
915 'Sigma_c*+',
'Sigma_c*0',
'Sigma_c*++']
and 'tag' not in get_plabel(p)],
916 [p
for p
in particles
if get_pname(p)
in [
'B0',
'B+',
'B_s0']
or 'tag' in get_plabel(p)],
922 if pname
not in [pname
for stage
in stages
for p
in stage]:
923 raise RuntimeError(f
"Unknown particle {pname}: Not implemented in FEI")
928def do_trainings(particles: typing.Sequence[
config.Particle], configuration: config.FeiConfiguration):
930 Performs the training of mva classifiers for all available training data,
931 this function must be either called by the user after each stage of the FEI during training,
932 or (more likely) is called by the distributed.py script after merging the outputs of all jobs,
933 @param particles list of config.Particle objects
934 @param config config.FeiConfiguration object
935 @return list of tuple with weight file on disk and identifier in database for all trained classifiers
937 teacher =
Teacher(particles, configuration)
938 return teacher.do_all_trainings()
942 configuration: config.FeiConfiguration,
944 roundMode: int =
None,
945 pickleName: str =
'Summary.pickle'):
947 Creates the Summary.pickle, which is used to keep track of the stage during the training,
948 and can be used later to investigate which configuration was used exactly to create the training.
949 @param particles list of config.Particle objects
950 @param config config.FeiConfiguration object
951 @param cache current cache level
952 @param roundMode mode of current round of training
953 @param pickleName name of the pickle file
955 if roundMode
is None:
956 roundMode = configuration.roundMode
957 configuration = configuration._replace(cache=cache, roundMode=roundMode)
959 for i
in range(8, -1, -1):
960 if os.path.isfile(f
'{pickleName}.backup_{i}'):
961 shutil.copyfile(f
'{pickleName}.backup_{i}', f
'{pickleName}.backup_{i+1}')
962 if os.path.isfile(pickleName):
963 shutil.copyfile(pickleName, f
'{pickleName}.backup_0')
964 pickle.dump((particles, configuration), open(pickleName,
'wb'))
967def get_path(particles: typing.Sequence[
config.Particle], configuration: config.FeiConfiguration) -> FeiState:
969 The most important function of the FEI.
970 This creates the FEI path for training/fitting (both terms are equal), and application/inference (both terms are equal).
971 The whole FEI is defined by the particles which are reconstructed (see default_channels.py)
972 and the configuration (see config.py).
975 For training this function is called multiple times, each time the FEI reconstructs one more stage in the hierarchical structure
976 i.e. we start with FSP, pi0, KS_0, D, D*, and with B mesons. You have to set configuration.training to True for training mode.
977 All weight files created during the training will be stored in your local database.
978 If you want to use the FEI training everywhere without copying this database by hand, you have to upload your local database
979 to the central database first (see documentation for the Belle2 Condition Database).
982 For application you call this function once, and it returns the whole path which will reconstruct B mesons
983 with an associated signal probability. You have to set configuration.training to False for application mode.
986 You can always turn on the monitoring (configuration.monitor != False),
987 to write out ROOT Histograms of many quantities for each stage,
988 using these histograms you can use the printReporting.py or latexReporting.py scripts to automatically create pdf files.
991 This function can also use old FEI trainings (version 3), just pass the Summary.pickle file of the old training,
992 and the weight files will be automatically converted to the new naming scheme.
994 @param particles list of config.Particle objects
995 @param config config.FeiConfiguration object
998 ____ _ _ _ _ ____ _ _ ____ _ _ ___ _ _ _ ___ ____ ____ ___ ____ ____ ___ ____ ___ _ ____ _ _
999 |___ | | | | |___ | | |___ |\ | | | |\ | | |___ |__/ |__] |__/ |___ | |__| | | | | |\ |
1000 | |__| |___ |___ |___ \/ |___ | \| | | | \| | |___ | \ | | \ |___ | | | | | |__| | \|
1002 Author: Thomas Keck 2014 - 2017
1003 Please cite my PhD thesis
1014 if configuration.training
and (configuration.monitor
and (configuration.monitoring_path !=
'')):
1015 B2ERROR(
"FEI-core: Custom Monitoring path is not allowed during training!")
1017 if configuration.cache
is None:
1018 pickleName =
'Summary.pickle'
1019 if configuration.monitor:
1020 pickleName = os.path.join(configuration.monitoring_path, pickleName)
1022 if os.path.isfile(pickleName):
1023 particles_bkp, config_bkp = pickle.load(open(pickleName,
'rb'))
1025 for fd
in configuration._fields:
1026 if fd ==
'cache' or fd ==
'roundMode':
1028 if getattr(configuration, fd) != getattr(config_bkp, fd):
1030 f
"FEI-core: Configuration changed: {fd} from {getattr(config_bkp, fd)} to {getattr(configuration, fd)}")
1032 configuration = config_bkp
1033 cache = configuration.cache
1034 print(
"Cache: Replaced particles from steering and configuration from Summary.pickle: ", cache, configuration.roundMode)
1036 if configuration.training:
1041 cache = configuration.cache
1044 path = basf2.create_path()
1049 stages = get_stages_from_particles(particles)
1054 if configuration.legacy
is not None:
1055 convert_legacy_training(particles, configuration)
1064 if cache < 0
and configuration.training:
1065 print(
"Stage 0: Run over all files to count the number of events and McParticles")
1066 path.add_path(training_data_information.reconstruct())
1067 if configuration.training:
1068 save_summary(particles, configuration, 0)
1069 return FeiState(path, 0, [], [], [])
1070 elif not configuration.training
and configuration.monitor:
1071 path.add_path(training_data_information.reconstruct())
1077 loader =
FSPLoader(particles, configuration)
1079 print(
"Stage 0: Load FSP particles")
1080 path.add_path(loader.reconstruct())
1103 for stage, stage_particles
in enumerate(stages):
1104 if len(stage_particles) == 0:
1105 print(f
"Stage {stage}: No particles to reconstruct in this stage, skipping!")
1112 print(f
"Stage {stage}: PreReconstruct particles: ", [p.name
for p
in stage_particles])
1113 path.add_path(pre_reconstruction.reconstruct())
1114 if configuration.training
and not (post_reconstruction.available()
and configuration.roundMode == 0):
1115 print(f
"Stage {stage}: Create training data for particles: ", [p.name
for p
in stage_particles])
1116 mc_counts = training_data_information.get_mc_counts()
1117 training_data =
TrainingData(stage_particles, configuration, mc_counts)
1118 path.add_path(training_data.reconstruct())
1119 used_lists += [channel.name
for particle
in stage_particles
for channel
in particle.channels]
1122 used_lists += [particle.identifier
for particle
in stage_particles]
1123 if (stage >= cache - 1)
and not ((configuration.roundMode == 1)
and configuration.training):
1124 if (configuration.roundMode == 3)
and configuration.training:
1125 print(f
"Stage {stage}: BDTs already applied for particles, no postReco needed: ", [p.name
for p
in stage_particles])
1127 print(f
"Stage {stage}: Apply BDT for particles: ", [p.name
for p
in stage_particles])
1128 if configuration.training
and not post_reconstruction.available():
1129 raise RuntimeError(
"FEI-core: training of current stage was not successful, please retrain!")
1130 path.add_path(post_reconstruction.reconstruct())
1131 if (((configuration.roundMode == 2)
or (configuration.roundMode == 3))
and configuration.training):
1133 fsps_of_all_stages = [fsp
for sublist
in get_stages_from_particles(loader.get_fsp_lists())
for fsp
in sublist]
1136 if configuration.training
and (configuration.roundMode == 3):
1137 dontRemove = used_lists + fsps_of_all_stages
1139 cleanup = basf2.register_module(
'RemoveParticlesNotInLists')
1140 print(
"FEI-REtrain: pruning basf2_input.root of higher stages")
1141 cleanup.param(
'particleLists', dontRemove)
1142 path.add_module(cleanup)
1146 excludedParticlesNonConjugated = [p.identifier
for p
in particles
if p.identifier
not in dontRemove]
1147 excludedParticles = [
1148 str(name)
for name
in list(
1149 ROOT.Belle2.ParticleListName.addAntiParticleLists(excludedParticlesNonConjugated))]
1150 root_file = ROOT.TFile.Open(
'basf2_input.root',
"READ")
1151 tree = root_file.Get(
'tree')
1152 for branch
in tree.GetListOfBranches():
1153 branchName = branch.GetName()
1154 if any(exParticle
in branchName
for exParticle
in excludedParticles):
1155 excludelists.append(branchName)
1156 print(
"Exclude lists from output: ", excludelists)
1160 if configuration.monitor:
1161 print(
"Add ModuleStatistics")
1162 output = basf2.register_module(
'RootOutput')
1163 output.param(
'outputFileName', os.path.join(configuration.monitoring_path,
'Monitor_ModuleStatistics.root'))
1164 output.param(
'branchNames', [
'EventMetaData'])
1165 output.param(
'branchNamesPersistent', [
'ProcessStatistics'])
1166 output.param(
'ignoreCommandLineOverride',
True)
1167 path.add_module(output)
1171 if configuration.training
or configuration.monitor:
1172 print(
"Save Summary.pickle")
1173 save_summary(particles, configuration, stage+1, pickleName=os.path.join(configuration.monitoring_path,
'Summary.pickle'))
1176 return FeiState(path, stage+1, plists=used_lists, fsplists=fsps_of_all_stages, excludelists=excludelists)
pybasf2.Path reconstruct(self)
__init__(self, typing.Sequence[config.Particle] particles, config.FeiConfiguration config)
typing.List[str] get_fsp_lists(self)
config
config.FeiConfiguration object
particles
list of config.Particle objects
pybasf2.Path reconstruct(self)
__init__(self, typing.Sequence[config.Particle] particles, config.FeiConfiguration config)
typing.Sequence[str] get_missing_channels(self)
config
config.FeiConfiguration object
particles
list of config.Particle objects
pybasf2.Path reconstruct(self)
__init__(self, typing.Sequence[config.Particle] particles, config.FeiConfiguration config)
config
config.FeiConfiguration object
particles
list of config.Particle objects
__init__(self, typing.Sequence[config.Particle] particles, config.FeiConfiguration config)
upload(self, str channel)
create_fake_weightfile(str channel)
config
config.FeiConfiguration object
particles
list of config.Particle objects
check_if_weightfile_is_fake(str filename)
pybasf2.Path reconstruct(self)
mc_counts
containing number of MC Particles
config
config.FeiConfiguration object
__init__(self, typing.Sequence[config.Particle] particles, config.FeiConfiguration config, typing.Mapping[int, typing.Mapping[str, float]] mc_counts)
particles
list of config.Particle objects