Belle II Software light-2607-kasei
core.py
1#!/usr/bin/env python3
2
3
10
11"""
12 The Full Event Interpretation Algorithm
13
14 Some basic facts:
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
20
21 Read this file if you want to understand the technical details of the FEI.
22
23 The FEI follows a hierarchical approach.
24 There are 7 stages:
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
28 Stage 2: K_S0, Sigma+
29 Stage 3: D and Lambda_c mesons
30 Stage 4: D* mesons
31 Stage 5: B mesons
32 Stage 6: Finish
33
34 Most stages consists of:
35 - Create Particle Candidates
36 - Apply Cuts
37 - Do vertex Fitting
38 - Apply a multivariate classification method
39 - Apply more Cuts
40
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.
44"""
45
46# Import basf2
47import basf2
48from basf2 import B2INFO, B2WARNING, B2ERROR
49import pybasf2
50import modularAnalysis as ma
51import b2bii
52
53# Should come after basf2 import
54import pdg
55from fei import config
56import basf2_mva
57
58# Standard python modules
59import collections
60import os
61import shutil
62import typing
63import pickle
64import importlib
65import re
66import functools
67import subprocess
68import multiprocessing
69
70# Simple object containing the output of fei
71FeiState = collections.namedtuple('FeiState', 'path, stage, plists, fsplists, excludelists')
72
73
75 """
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.
83 """
84
85 def __init__(self, particles: typing.Sequence[config.Particle], outputPath: str = ''):
86 """
87 Create a new TrainingData object
88 @param particles list of config.Particle objects
89 @param outputPath path to the output directory
90 """
91
92 self.particles = particles
93
94 self.filename = os.path.join(outputPath, 'mcParticlesCount.root')
95
96 def available(self) -> bool:
97 """
98 Check if the relevant information is already available
99 """
100 return os.path.isfile(self.filename)
101
102 def reconstruct(self) -> pybasf2.Path:
103 """
104 Returns pybasf2.Path which counts the number of MCParticles in each event.
105 @param particles list of config.Particle objects
106 """
107 # Unique absolute pdg-codes of all particles
108 pdgs = {abs(pdg.from_name(particle.name)) for particle in self.particles}
109
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)
117 return path
118
119 def get_mc_counts(self):
120 """
121 Read out the number of MC particles from the file created by reconstruct
122 """
123 # Unique absolute pdg-codes of all particles
124 # Always avoid the top-level 'import ROOT'.
125 import ROOT # noqa
126 root_file = ROOT.TFile.Open(self.filename, 'read')
127 mc_counts = {}
128
129 for key in root_file.GetListOfKeys():
130 variable = ROOT.Belle2.MakeROOTCompatible.invertMakeROOTCompatible(key.GetName())
131 pdg = abs(int(variable[len('NumberOfMCParticlesInEvent('):-len(")")]))
132 hist = key.ReadObj()
133 mc_counts[pdg] = {}
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))
140
141 mc_counts[0] = {}
142 mc_counts[0]['sum'] = hist.GetEntries() # this is the total number of ALL events, does not matter which hist we take
143 root_file.Close()
144 return mc_counts
145
146
148 """
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).
153 """
154
155 def __init__(self, particles: typing.Sequence[config.Particle], config: config.FeiConfiguration):
156 """
157 Create a new FSPLoader object
158 @param particles list of config.Particle objects
159 @param config config.FeiConfiguration object
160 """
161
162 self.particles = particles
163
164 self.config = config
165
166 def get_fsp_lists(self) -> typing.List[str]:
167 """
168 Returns a list of FSP particle lists which are used in the FEI.
169 This is used to create the RootOutput module.
170 """
171 fsps = ['K+:FSP', 'pi+:FSP', 'e+:FSP', 'mu+:FSP', 'p+:FSP', 'gamma:FSP', 'K_S0:V0', 'Lambda0:V0', 'K_L0:FSP', 'gamma:V0']
172 if b2bii.isB2BII():
173 fsps += ['pi0:FSP']
174 return fsps
175
176 def reconstruct(self) -> pybasf2.Path:
177 """
178 Returns pybasf2.Path which loads the FSP Particles
179 """
180 path = basf2.create_path()
181
182 if b2bii.isB2BII():
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)
189 else:
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)
196
197 if self.config.monitor:
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')
200 pdgs = {abs(pdg.from_name(name)) for name in names}
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)
203 return path
204
205
207 """
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).
212 """
213
214 def __init__(self, particles: typing.Sequence[config.Particle], config: config.FeiConfiguration,
215 mc_counts: typing.Mapping[int, typing.Mapping[str, float]]):
216 """
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
221 """
222
223 self.particles = particles
224
225 self.config = config
226
227 self.mc_counts = mc_counts
228
229 def reconstruct(self) -> pybasf2.Path:
230 """
231 Returns pybasf2.Path which creates the training data for the given particles
232 """
233 import ROOT # noqa
234 path = basf2.create_path()
235
236 for particle in self.particles:
237 pdgcode = abs(pdg.from_name(particle.name))
238 nSignal = self.mc_counts[pdgcode]['sum']
239 print(f"FEI-core: TrainingData: nSignal for {particle.name}: {nSignal}")
240
241 # For D-Mesons we usually have a efficiency of 10^-3 including branching fraction
242 if pdgcode > 400:
243 nSignal /= 1000
244 # For B-Mesons we usually have a efficiency of 10^-4 including branching fraction
245 if pdgcode > 500:
246 nSignal /= 10000
247
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")
252 continue
253 filename = 'training_input.root'
254
255 # nBackground = nEvents * nBestCandidates
256 nBackground = self.mc_counts[0]['sum'] * channel.preCutConfig.bestCandidateCut
257 inverseSamplingRates = {}
258 # For some very pure channels (Jpsi), this sampling can be too aggressive and training fails.
259 # It can therefore be disabled in the preCutConfig.
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)
265
266 if nSignal > Teacher.MaximumNumberOfMVASamples and not channel.preCutConfig.noSignalSampling:
267 inverseSamplingRates[1] = int(nSignal / Teacher.MaximumNumberOfMVASamples) + 1
268
269 spectators = [channel.mvaConfig.target] + list(channel.mvaConfig.spectators.keys())
270 if channel.mvaConfig.sPlotVariable is not None:
271 spectators.append(channel.mvaConfig.sPlotVariable)
272
273 if self.config.monitor:
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)
283
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)
293 return path
294
295
297 """
298 Steers the reconstruction phase before the mva method was applied
299 It Includes:
300 - The ParticleCombination (for each particle and channel we create candidates using
301 the daughter candidates from the previous stages)
302 - MC Matching
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)
306 """
307
308 def __init__(self, particles: typing.Sequence[config.Particle], config: config.FeiConfiguration):
309 """
310 Create a new PreReconstruction object
311 @param particles list of config.Particle objects
312 @param config config.FeiConfiguration object
313 """
314
315 self.particles = particles
316
317 self.config = config
318
319 def reconstruct(self) -> pybasf2.Path:
320 """
321 Returns pybasf2.Path which reconstructs the particles and does the vertex fitting if necessary
322 """
323 path = basf2.create_path()
324
325 for particle in self.particles:
326 for channel in particle.channels:
327
328 if (len(channel.daughters) == 1) and (pdg.from_name(
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'})
335 # suppress warning that decay mode ID won't be overwritten if it already exists
336 v2EI.set_log_level(basf2.logging.log_level.ERROR)
337 path.add_module(v2EI)
338 else:
339 ma.reconstructDecay(channel.decayString, channel.preCutConfig.userCut, channel.decayModeID,
340 writeOut=True, path=path)
341
342 # optionaly here we run custom modules, that users provide by themselves
343 # in the channels list (useful for testing custom variables)
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"])
349
350 if self.config.monitor:
351 if "tag" in (channel.name).lower():
352 ma.matchTagTruth(channel.name, path=path)
353 else:
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)')]
359 else:
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(
370 channel.name,
371 variables=config.variables2binnings(hist_variables),
372 variables_2d=config.variables2binnings_2d(hist_variables_2d),
373 filename=filename,
374 ignoreCommandLineOverride=True,
375 directory=f'{channel.label}',
376 path=path)
377
378 if channel.preCutConfig.bestCandidateMode == 'lowest':
379 ma.rankByLowest(channel.name,
380 channel.preCutConfig.bestCandidateVariable,
381 channel.preCutConfig.bestCandidateCut,
382 'preCut_rank',
383 path=path)
384 elif channel.preCutConfig.bestCandidateMode == 'highest':
385 ma.rankByHighest(channel.name,
386 channel.preCutConfig.bestCandidateVariable,
387 channel.preCutConfig.bestCandidateCut,
388 'preCut_rank',
389 path=path)
390 else:
391 raise RuntimeError(f'Unknown bestCandidateMode {repr(channel.preCutConfig.bestCandidateMode)}')
392
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)
399
400 matches = list(re.finditer('gamma', channel.decayString))
401 pi0lists = []
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(
412 'pi0:veto',
413 {
414 'InvM': 'pi0vetoMass',
415 'formula((daughter(0,E)-daughter(1,E))/(daughter(0,E)+daughter(1,E)))': 'pi0vetoEneAsy',
416 'cosHelicityAngleMomentum': 'pi0vetoCosHelMom',
417 },
418 path=Ddaughter_roe_path
419 )
420 path.for_each('RestOfEvent', 'RestOfEvents', Ddaughter_roe_path)
421
422 if self.config.monitor:
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(
432 channel.name,
433 variables=config.variables2binnings(hist_variables),
434 variables_2d=config.variables2binnings_2d(hist_variables_2d),
435 filename=filename,
436 ignoreCommandLineOverride=True,
437 directory=f'{channel.label}',
438 path=path)
439 # If we are not in monitor mode we do the mc matching now,
440 # otherwise we did it above already!
441 elif self.config.training:
442 if "tag" in (channel.name).lower():
443 ma.matchTagTruth(channel.name, path=path)
444 else:
445 ma.matchMCTruth(channel.name, path=path)
446
447 if b2bii.isB2BII() and particle.name in ['K_S0', 'Lambda0']:
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) # let's not produce gigabytes of uninteresting warnings
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')
466 else:
467 pvfit.param('fitType', 'vertex')
468 pvfit.set_log_level(basf2.logging.log_level.ERROR) # let's not produce gigabytes of uninteresting warnings
469 path.add_module(pvfit)
470
471 if self.config.monitor:
472 if self.config.monitor == 'simple':
473 hist_variables = [channel.mvaConfig.target, 'extraInfo(decayModeID)']
474 hist_variables_2d = [(channel.mvaConfig.target, 'extraInfo(decayModeID)')]
475 else:
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(
486 channel.name,
487 variables=config.variables2binnings(hist_variables),
488 variables_2d=config.variables2binnings_2d(hist_variables_2d),
489 filename=filename,
490 ignoreCommandLineOverride=True,
491 directory=f'{channel.label}',
492 path=path)
493
494 return path
495
496
498 """
499 Steers the reconstruction phase after the mva method was applied
500 It Includes:
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
504 """
505
506 def __init__(self, particles: typing.Sequence[config.Particle], config: config.FeiConfiguration):
507 """
508 Create a new PostReconstruction object
509 @param particles list of config.Particle objects
510 @param config config.FeiConfiguration object
511 """
512
513 self.particles = particles
514
515 self.config = config
516
517 def get_missing_channels(self) -> typing.Sequence[str]:
518 """
519 Returns all channels for which the weightfile is missing
520 """
521 missing = []
522 for particle in self.particles:
523 for channel in particle.channels:
524 # weightfile = self.config.prefix + '_' + channel.label
525 weightfile = f'{channel.label}.xml'
526 if not basf2_mva.available(weightfile):
527 missing += [channel.label]
528 return missing
529
530 def available(self) -> bool:
531 """
532 Check if the relevant information is already available
533 """
534 return len(self.get_missing_channels()) == 0
535
536 def reconstruct(self) -> pybasf2.Path:
537 """
538 Returns pybasf2.Path which reconstructs the particles and does the vertex fitting if necessary
539 """
540 import ROOT # noqa
541 path = basf2.create_path()
542
543 for particle in self.particles:
544 for channel in particle.channels:
545 expert = basf2.register_module('MVAExpert')
546 expert.set_name(f'MVAExpert_{channel.name}')
547 if self.config.training:
548 expert.param('identifier', f'{channel.label}.xml')
549 else:
550 expert.param('identifier', f'{self.config.prefix}_{channel.label}')
551 expert.param('extraInfoName', 'SignalProbability')
552 expert.param('listNames', [channel.name])
553 # suppress warning that signal probability won't be overwritten if it already exists
554 expert.set_log_level(basf2.logging.log_level.ERROR)
555 path.add_module(expert)
556
557 if self.config.monitor:
558 if self.config.monitor == 'simple':
559 hist_variables = [channel.mvaConfig.target, 'extraInfo(decayModeID)']
560 hist_variables_2d = [(channel.mvaConfig.target, 'extraInfo(decayModeID)')]
561 else:
562 hist_variables = ['mcErrors',
563 'mcParticleStatus',
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(
579 channel.name,
580 variables=config.variables2binnings(hist_variables),
581 variables_2d=config.variables2binnings_2d(hist_variables_2d),
582 filename=filename,
583 ignoreCommandLineOverride=True,
584 directory=f'{channel.label}',
585 path=path)
586
587 cutstring = ''
588 if particle.postCutConfig.value > 0.0:
589 cutstring = f'{particle.postCutConfig.value} < extraInfo(SignalProbability)'
590
591 ma.mergeListsWithBestDuplicate(particle.identifier, [c.name for c in particle.channels],
592 variable='particleSource', writeOut=True, path=path)
593
594 if self.config.monitor:
595 if self.config.monitor == 'simple':
596 hist_variables = [particle.mvaConfig.target, 'extraInfo(decayModeID)']
597 hist_variables_2d = [(particle.mvaConfig.target, 'extraInfo(decayModeID)')]
598 else:
599 hist_variables = ['mcErrors',
600 'mcParticleStatus',
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(
613 particle.identifier,
614 variables=config.variables2binnings(hist_variables),
615 variables_2d=config.variables2binnings_2d(hist_variables_2d),
616 filename=filename,
617 ignoreCommandLineOverride=True,
618 directory=config.removeJPsiSlash(f'{particle.identifier}'),
619 path=path)
620
621 ma.applyCuts(particle.identifier, cutstring, path=path)
622
623 if self.config.monitor:
624 filename = os.path.join(self.config.monitoring_path, 'Monitor_PostReconstruction_BeforeRanking.root')
625 ma.variablesToHistogram(
626 particle.identifier,
627 variables=config.variables2binnings(hist_variables),
628 variables_2d=config.variables2binnings_2d(hist_variables_2d),
629 filename=filename,
630 ignoreCommandLineOverride=True,
631 directory=config.removeJPsiSlash(f'{particle.identifier}'),
632 path=path)
633
634 ma.rankByHighest(particle.identifier, 'extraInfo(SignalProbability)',
635 particle.postCutConfig.bestCandidateCut, 'postCut_rank', path=path)
636
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}')
642 # suppress warning that unique signal extra info won't be overwritten if it already exists
643 uniqueSignal.set_log_level(basf2.logging.log_level.ERROR)
644 path.add_module(uniqueSignal)
645
646 if self.config.monitor:
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(
657 particle.identifier,
658 variables=config.variables2binnings(hist_variables),
659 variables_2d=config.variables2binnings_2d(hist_variables_2d),
660 filename=filename,
661 ignoreCommandLineOverride=True,
662 directory=config.removeJPsiSlash(f'{particle.identifier}'),
663 path=path)
664
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(
670 particle.identifier,
671 variables=config.variables2binnings(hist_variables),
672 variables_2d=config.variables2binnings_2d(hist_variables_2d),
673 filename=filename,
674 ignoreCommandLineOverride=True,
675 directory=config.removeJPsiSlash(f'{particle.identifier}'),
676 path=path)
677 else:
678 variables = ['extraInfo(SignalProbability)', 'mcErrors', 'mcParticleStatus', particle.mvaConfig.target,
679 'extraInfo(uniqueSignal)', 'extraInfo(decayModeID)'] + list(particle.mvaConfig.spectators.keys())
680
681 ma.variablesToNtuple(
682 particle.identifier,
683 variables,
684 treename=ROOT.Belle2.MakeROOTCompatible.makeROOTCompatible(
685 config.removeJPsiSlash(f'{particle.identifier} variables')),
686 filename=filename,
687 ignoreCommandLineOverride=True,
688 path=path)
689 return path
690
691
693 """
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
701 """
702
704 MaximumNumberOfMVASamples = int(1e7)
705
707 MinimumNumberOfMVASamples = int(5e2)
708
709 def __init__(self, particles: typing.Sequence[config.Particle], config: config.FeiConfiguration):
710 """
711 Create a new Teacher object
712 @param particles list of config.Particle objects
713 @param config config.FeiConfiguration object
714 """
715
716 self.particles = particles
717
718 self.config = config
719
720 @staticmethod
721 def create_fake_weightfile(channel: str):
722 """
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
725 """
726 content = f"""
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>
743 """
744 with open(f'{channel}.xml', "w") as f:
745 f.write(content)
746
747 @staticmethod
748 def check_if_weightfile_is_fake(filename: str):
749 """
750 Checks if the provided filename is a fake-weight file or not
751 @param filename the filename of the weight file
752 """
753 try:
754 return '<method>Trivial</method>' in open(filename).readlines()[2]
755 except BaseException:
756 return True
757 return True
758
759 def upload(self, channel: str):
760 """
761 Upload the weight file into the condition database
762 @param channel whose weight file is uploaded
763 """
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")
768 return (disk, dbase)
769
771 """
772 Do all trainings for which we find training data
773 """
774 # Always avoid the top-level 'import ROOT'.
775 import ROOT # noqa
776 # FEI uses multi-threading for parallel execution of tasks therefore
777 # the ROOT gui-thread is disabled, which otherwise interferes sometimes
778 ROOT.PyConfig.StartGuiThread = False
779 job_list = []
780
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)
784 else:
785 stagesToTrain = [self.config.cache]
786
787 filename = 'training_input.root'
788 if os.path.isfile(filename):
789 f = ROOT.TFile.Open(filename, 'read')
790 if f.IsZombie():
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:
793 B2WARNING(
794 f'Training of MVC failed: {filename}. ROOT file has no trees. No weight files will be provided.')
795 else:
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")
802 continue
803 else:
804 treeName = ROOT.Belle2.MakeROOTCompatible.makeROOTCompatible(f'{channel.label} variables')
805 keys = [m for m in f.GetListOfKeys() if treeName in m.GetName()]
806 if not keys:
807 B2WARNING("Training of MVC failed. "
808 f"Couldn't find tree for channel {channel}. Ignoring channel.")
809 continue
810 elif len(keys) > 1:
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')
816 B2INFO(
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}.")
822 self.create_fake_weightfile(channel.label)
823 self.upload(channel.label)
824 continue
825 if nBg < Teacher.MinimumNumberOfMVASamples:
826 B2WARNING("Training of MVC failed. "
827 f"Tree contains too few bckgrd events {nBg}. Ignoring channel {channel}.")
828 self.create_fake_weightfile(channel.label)
829 self.upload(channel.label)
830 continue
831 variable_str = "' '".join(channel.mvaConfig.variables)
832
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)
837
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'"
844 f" --signal_class 1"
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))
852 f.Close()
853
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])
858 p.close()
859 p.join()
860 weightfiles = []
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')
864 self.create_fake_weightfile(name)
865 weightfiles.append(self.upload(name))
866 return weightfiles
867
868
869def convert_legacy_training(particles: typing.Sequence[config.Particle], configuration: config.FeiConfiguration):
870 """
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
876 """
877 summary = pickle.load(open(configuration.legacy, 'rb'))
878 channel2lists = {k: v[2] for k, v in summary['channel2lists'].items()}
879
880 teacher = Teacher(particles, configuration)
881
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)
890 else:
891 basf2_mva.download(old_weightfile, f'{channel.label}.xml')
892 teacher.upload(channel.label)
893
894
895def get_stages_from_particles(particles: typing.Sequence[typing.Union[config.Particle, str]]):
896 """
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
901 """
902 def get_pname(p):
903 return p.split(":")[0] if isinstance(p, str) else p.name
904
905 def get_plabel(p):
906 return (p.split(":")[1] if isinstance(p, str) else p.label).lower()
907
908 stages = [
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)],
917 []
918 ]
919
920 for p in particles:
921 pname = get_pname(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")
924
925 return stages
926
927
928def do_trainings(particles: typing.Sequence[config.Particle], configuration: config.FeiConfiguration):
929 """
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
936 """
937 teacher = Teacher(particles, configuration)
938 return teacher.do_all_trainings()
939
940
941def save_summary(particles: typing.Sequence[config.Particle],
942 configuration: config.FeiConfiguration,
943 cache: int,
944 roundMode: int = None,
945 pickleName: str = 'Summary.pickle'):
946 """
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
954 """
955 if roundMode is None:
956 roundMode = configuration.roundMode
957 configuration = configuration._replace(cache=cache, roundMode=roundMode)
958 # Backup existing Summary.pickle files
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'))
965
966
967def get_path(particles: typing.Sequence[config.Particle], configuration: config.FeiConfiguration) -> FeiState:
968 """
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).
973
974 TRAINING
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).
980
981 APPLICATION
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.
984
985 MONITORING
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.
989
990 LEGACY
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.
993
994 @param particles list of config.Particle objects
995 @param config config.FeiConfiguration object
996 """
997 print(r"""
998 ____ _ _ _ _ ____ _ _ ____ _ _ ___ _ _ _ ___ ____ ____ ___ ____ ____ ___ ____ ___ _ ____ _ _
999 |___ | | | | |___ | | |___ |\ | | | |\ | | |___ |__/ |__] |__/ |___ | |__| | | | | |\ |
1000 | |__| |___ |___ |___ \/ |___ | \| | | | \| | |___ | \ | | \ |___ | | | | | |__| | \|
1001
1002 Author: Thomas Keck 2014 - 2017
1003 Please cite my PhD thesis
1004 """)
1005
1006 # The cache parameter of the configuration object is used during training to keep track,
1007 # which reconstruction steps are already performed.
1008 # For fitting/training we start by default with -1, meaning we still have to create the TrainingDataInformation,
1009 # which is used to determine the number of candidates we have to write out for the FSP trainings in stage 0.
1010 # For inference/application we start by default with 0, because we don't need the TrainingDataInformation in stage 0.
1011 # RoundMode plays a similar role as cache,
1012 # it is used to keep track in which phase within a stage the basf2 execution stops, relevant only for training.
1013 # During the training we save the particles and configuration (including the current cache stage) in the Summary.pickle object.
1014 if configuration.training and (configuration.monitor and (configuration.monitoring_path != '')):
1015 B2ERROR("FEI-core: Custom Monitoring path is not allowed during training!")
1016
1017 if configuration.cache is None:
1018 pickleName = 'Summary.pickle'
1019 if configuration.monitor:
1020 pickleName = os.path.join(configuration.monitoring_path, pickleName)
1021
1022 if os.path.isfile(pickleName):
1023 particles_bkp, config_bkp = pickle.load(open(pickleName, 'rb'))
1024 # check if configuration changed
1025 for fd in configuration._fields:
1026 if fd == 'cache' or fd == 'roundMode':
1027 continue
1028 if getattr(configuration, fd) != getattr(config_bkp, fd):
1029 B2WARNING(
1030 f"FEI-core: Configuration changed: {fd} from {getattr(config_bkp, fd)} to {getattr(configuration, fd)}")
1031
1032 configuration = config_bkp
1033 cache = configuration.cache
1034 print("Cache: Replaced particles from steering and configuration from Summary.pickle: ", cache, configuration.roundMode)
1035 else:
1036 if configuration.training:
1037 cache = -1
1038 else:
1039 cache = 0
1040 else:
1041 cache = configuration.cache
1042
1043 # Now we start building the training or application path
1044 path = basf2.create_path()
1045
1046 # There are in total 7 stages.
1047 # For training we start with -1 and go to 7 one stage at a time
1048 # For application we can run stage 0 to 7 at once
1049 stages = get_stages_from_particles(particles)
1050
1051 # If the user provided a Summary.pickle file of a FEIv3 training we
1052 # convert the old weight files (with hashes), to the new naming scheme.
1053 # Afterwards the algorithm runs as usual
1054 if configuration.legacy is not None:
1055 convert_legacy_training(particles, configuration)
1056
1057 # During the training we require the number of MC particles in the whole processed
1058 # data sample, because we don't want to write out billions of e.g. pion candidates.
1059 # Knowing the total amount of MC particles we can write out only every e.g. 10th candidate
1060 # That's why we have to write out the TrainingDataInformation before doing anything during the training phase.
1061 # During application we only need this if we run in monitor mode, and want to write out a summary in the end,
1062 # the summary contains efficiency, and the efficiency calculation requires the total number of MC particles.
1063 training_data_information = TrainingDataInformation(particles, outputPath=configuration.monitoring_path)
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())
1072
1073 # We load the Final State particles
1074 # It is assumed that the user takes care of adding RootInput, Geometry, and everything
1075 # which is required to read in data, so we directly start to load the FSP particles
1076 # used by the FEI.
1077 loader = FSPLoader(particles, configuration)
1078 if cache < 1:
1079 print("Stage 0: Load FSP particles")
1080 path.add_path(loader.reconstruct())
1081
1082 # Now we reconstruct each stage one after another.
1083 # Each stage consists of two parts:
1084 # PreReconstruction (before the mva method was applied):
1085 # - Particle combination
1086 # - Do vertex fitting
1087 # - Some simple cuts and best candidate selection
1088 # PostReconstruction (after the mva method was applied):
1089 # - Apply the mva method
1090 # - Apply cuts on the mva output and best candidate selection
1091 #
1092 # If the weight files for the PostReconstruction are not available for the current stage and we are in training mode,
1093 # we have to create the training data. The training itself is done by the do_trainings function which is called
1094 # either by the user after each step, or by the distributed.py script
1095 #
1096 # If the weight files for the PostReconstruction are not available for the current stage and we are not in training mode,
1097 # we keep going, as soon as the user will call process on the produced path he will get an error message that the
1098 # weight files are missing.
1099 #
1100 # Finally we keep track of the ParticleLists we use, so the user can run the RemoveParticles module to reduce the size of the
1101 # intermediate output of RootOutput.
1102 used_lists = []
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!")
1106 continue
1107
1108 pre_reconstruction = PreReconstruction(stage_particles, configuration)
1109 post_reconstruction = PostReconstruction(stage_particles, configuration)
1110
1111 if stage >= cache:
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]
1120 break
1121
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])
1126 else:
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):
1132 break
1133 fsps_of_all_stages = [fsp for sublist in get_stages_from_particles(loader.get_fsp_lists()) for fsp in sublist]
1134
1135 excludelists = []
1136 if configuration.training and (configuration.roundMode == 3):
1137 dontRemove = used_lists + fsps_of_all_stages
1138 # cleanup higher 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)
1143
1144 # check which lists we have to exclude from the output
1145 import ROOT # noqa
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)
1157
1158 # If we run in monitor mode we are interested in the ModuleStatistics,
1159 # these statistics contain the runtime for each module which was run.
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']) # cannot be removed, but of only small effect on file size
1165 output.param('branchNamesPersistent', ['ProcessStatistics'])
1166 output.param('ignoreCommandLineOverride', True)
1167 path.add_module(output)
1168
1169 # As mentioned above the FEI keeps track of the stages which are already reconstructed during the training
1170 # so we write out the Summary.pickle here, and increase the stage by one.
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'))
1174
1175 # Finally we return the path, the stage and the used lists to the user.
1176 return FeiState(path, stage+1, plists=used_lists, fsplists=fsps_of_all_stages, excludelists=excludelists)
isB2BII()
Definition b2bii.py:14
pybasf2.Path reconstruct(self)
Definition core.py:176
__init__(self, typing.Sequence[config.Particle] particles, config.FeiConfiguration config)
Definition core.py:155
typing.List[str] get_fsp_lists(self)
Definition core.py:166
config
config.FeiConfiguration object
Definition core.py:164
particles
list of config.Particle objects
Definition core.py:162
pybasf2.Path reconstruct(self)
Definition core.py:536
__init__(self, typing.Sequence[config.Particle] particles, config.FeiConfiguration config)
Definition core.py:506
typing.Sequence[str] get_missing_channels(self)
Definition core.py:517
config
config.FeiConfiguration object
Definition core.py:515
particles
list of config.Particle objects
Definition core.py:513
pybasf2.Path reconstruct(self)
Definition core.py:319
__init__(self, typing.Sequence[config.Particle] particles, config.FeiConfiguration config)
Definition core.py:308
config
config.FeiConfiguration object
Definition core.py:317
particles
list of config.Particle objects
Definition core.py:315
__init__(self, typing.Sequence[config.Particle] particles, config.FeiConfiguration config)
Definition core.py:709
upload(self, str channel)
Definition core.py:759
create_fake_weightfile(str channel)
Definition core.py:721
do_all_trainings(self)
Definition core.py:770
config
config.FeiConfiguration object
Definition core.py:718
particles
list of config.Particle objects
Definition core.py:716
check_if_weightfile_is_fake(str filename)
Definition core.py:748
pybasf2.Path reconstruct(self)
Definition core.py:102
__init__(self, typing.Sequence[config.Particle] particles, str outputPath='')
Definition core.py:85
particles
list of config.Particle objects
Definition core.py:92
pybasf2.Path reconstruct(self)
Definition core.py:229
mc_counts
containing number of MC Particles
Definition core.py:227
config
config.FeiConfiguration object
Definition core.py:225
__init__(self, typing.Sequence[config.Particle] particles, config.FeiConfiguration config, typing.Mapping[int, typing.Mapping[str, float]] mc_counts)
Definition core.py:215
particles
list of config.Particle objects
Definition core.py:223
from_name(name)
Definition pdg.py:63