Loading [MathJax]/jax/output/SVG/config.js
ATLAS Offline Software
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
ElectronAnalysisConfig.py
Go to the documentation of this file.
1 # Copyright (C) 2002-2025 CERN for the benefit of the ATLAS collaboration
2 
3 # AnaAlgorithm import(s):
4 from AnalysisAlgorithmsConfig.ConfigBlock import ConfigBlock
5 from AthenaCommon.SystemOfUnits import GeV
6 from AthenaConfiguration.Enums import LHCPeriod
7 from AnalysisAlgorithmsConfig.ConfigAccumulator import DataType
8 from TrigGlobalEfficiencyCorrection.TriggerLeg_DictHelpers import TriggerDict, MapKeysDict
9 from Campaigns.Utils import Campaign
10 from AthenaCommon.Logging import logging
11 
12 # E/gamma import(s).
13 from xAODEgamma.xAODEgammaParameters import xAOD
14 
16 
17 
18 class ElectronCalibrationConfig (ConfigBlock) :
19  """the ConfigBlock for the electron four-momentum correction"""
20 
21  def __init__ (self, containerName='') :
22  super (ElectronCalibrationConfig, self).__init__ ()
23  self.setBlockName('Electrons')
24  self.addOption ('inputContainer', '', type=str,
25  info="select electron input container, by default set to Electrons")
26  self.addOption ('containerName', containerName, type=str,
27  noneAction='error',
28  info="the name of the output container after calibration.")
29  self.addOption ('ESModel', '', type=str,
30  info="flag of egamma calibration recommendation.")
31  self.addOption ('decorrelationModel', '1NP_v1', type=str,
32  info="egamma energy scale decorrelationModel. The default is 1NP_v1. "
33  "Supported Model: 1NP_v1, FULL_v1.")
34  self.addOption ('postfix', '', type=str,
35  info="a postfix to apply to decorations and algorithm names. Typically "
36  "not needed here since the calibration is common to all electrons.")
37  self.addOption ('crackVeto', False, type=bool,
38  info="whether to perform LAr crack veto based on the cluster eta, "
39  "i.e. remove electrons within 1.37<|eta|<1.52. The default "
40  "is False.")
41  self.addOption ('isolationCorrection', False, type=bool,
42  info="whether or not to perform isolation corrections (leakage "
43  "corrections), i.e. set up an instance of "
44  "CP::EgammaIsolationCorrectionAlg.")
45  self.addOption ('recalibratePhyslite', True, type=bool,
46  info="whether to run the CP::EgammaCalibrationAndSmearingAlg on "
47  "PHYSLITE derivations. The default is True.")
48  self.addOption ('minPt', 4.5*GeV, type=float,
49  info="the minimum pT cut to apply to calibrated electrons. "
50  "The default is 4.5 GeV.")
51  self.addOption ('maxEta', 2.47, type=float,
52  info="maximum electron |eta| (float). The default is 2.47.")
53  self.addOption ('forceFullSimConfig', False, type=bool,
54  info="whether to force the tool to use the configuration meant for "
55  "full simulation samples. Only for testing purposes. The default "
56  "is False.")
57 
58  self.addOption ('splitCalibrationAndSmearing', False, type=bool,
59  info="EXPERIMENTAL: This splits the EgammaCalibrationAndSmearingTool "
60  " into two steps. The first step applies a baseline calibration that "
61  "is not affected by systematics. The second step then applies the "
62  "systematics dependent corrections. The net effect is that the "
63  "slower first step only has to be run once, while the second is run "
64  "once per systematic. ATLASG-2358")
65 
66  self.addOption ('decorateTruth', False, type=bool,
67  info="decorate truth particle information on the reconstructed one")
68  self.addOption ('decorateCaloClusterEta', False, type=bool,
69  info="decorate the calo cluster eta on the reconstructed one")
70  self.addOption ('writeTrackD0Z0', False, type = bool,
71  info="save the d0 significance and z0sinTheta variables so they can be written out")
72  self.addOption ('decorateEmva', False, type=bool,
73  info="decorate E_mva_only on the objects (needed for columnar tools/PHYSLITE)")
74 
75  self.addOption ('decorateSamplingPattern', False, type=bool,
76  info="add samplingPattern decorations to clusters as part of PHYSLITE")
77 
78 
79  def makeCalibrationAndSmearingAlg (self, config, name) :
80  """Create the calibration and smearing algorithm
81 
82  Factoring this out into its own function, as we want to
83  instantiate it in multiple places"""
84  log = logging.getLogger('ElectronCalibrationConfig')
85 
86  # Set up the calibration and smearing algorithm:
87  alg = config.createAlgorithm( 'CP::EgammaCalibrationAndSmearingAlg', name + self.postfix )
88  config.addPrivateTool( 'calibrationAndSmearingTool',
89  'CP::EgammaCalibrationAndSmearingTool' )
90  # Set default ESModel per period
91  if self.ESModel:
92  alg.calibrationAndSmearingTool.ESModel = self.ESModel
93  else:
94  if config.geometry() is LHCPeriod.Run2:
95  alg.calibrationAndSmearingTool.ESModel = 'es2023_R22_Run2_v1'
96  elif config.geometry() is LHCPeriod.Run3:
97  alg.calibrationAndSmearingTool.ESModel = 'es2022_R22_PRE'
98  elif config.geometry() is LHCPeriod.Run4:
99  log.warning("No ESModel set for Run4, using Run 3 model instead")
100  alg.calibrationAndSmearingTool.ESModel = 'es2022_R22_PRE'
101  else:
102  raise ValueError (f"Can't set up the ElectronCalibrationConfig with {config.geometry().value}, "
103  "there must be something wrong!")
104 
105  alg.calibrationAndSmearingTool.decorrelationModel = self.decorrelationModel
106  alg.calibrationAndSmearingTool.useFastSim = (
107  0 if self.forceFullSimConfig
108  else int( config.dataType() is DataType.FastSim ))
109  alg.calibrationAndSmearingTool.decorateEmva = self.decorateEmva
110  alg.egammas = config.readName (self.containerName)
111  alg.egammasOut = config.copyName (self.containerName)
112  alg.preselection = config.getPreselection (self.containerName, '')
113  return alg
114 
115 
116  def makeAlgs (self, config) :
117 
118  log = logging.getLogger('ElectronCalibrationConfig')
119 
120  if self.forceFullSimConfig:
121  log.warning("You are running ElectronCalibrationConfig forcing full sim config")
122  log.warning(" This is only intended to be used for testing purposes")
123 
124  inputContainer = "AnalysisElectrons" if config.isPhyslite() else "Electrons"
125  if self.inputContainer:
126  inputContainer = self.inputContainer
127  config.setSourceName (self.containerName, inputContainer)
128 
129  # Decorate calo cluster eta if required
130  if self.decorateCaloClusterEta:
131  alg = config.createAlgorithm( 'CP::EgammaCaloClusterEtaAlg',
132  'ElectronEgammaCaloClusterEtaAlg' + self.postfix,
133  reentrant=True )
134  alg.particles = config.readName(self.containerName)
135  config.addOutputVar (self.containerName, 'caloEta2', 'caloEta2', noSys=True)
136 
137  if self.decorateSamplingPattern:
138  config.createAlgorithm( 'CP::EgammaSamplingPatternDecoratorAlg', 'EgammaSamplingPatternDecoratorAlg' + self.postfix )
139 
140  # Set up a shallow copy to decorate
141  if config.wantCopy (self.containerName) :
142  alg = config.createAlgorithm( 'CP::AsgShallowCopyAlg', 'ElectronShallowCopyAlg' + self.postfix )
143  alg.input = config.readName (self.containerName)
144  alg.output = config.copyName (self.containerName)
145 
146  # Set up the eta-cut on all electrons prior to everything else
147  alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronEtaCutAlg' + self.postfix )
148  alg.selectionDecoration = 'selectEta' + self.postfix + ',as_bits'
149  config.addPrivateTool( 'selectionTool', 'CP::AsgPtEtaSelectionTool' )
150  alg.selectionTool.maxEta = self.maxEta
151  if self.crackVeto:
152  alg.selectionTool.etaGapLow = 1.37
153  alg.selectionTool.etaGapHigh = 1.52
154  alg.selectionTool.useClusterEta = True
155  alg.particles = config.readName (self.containerName)
156  alg.preselection = config.getPreselection (self.containerName, '')
157  config.addSelection (self.containerName, '', alg.selectionDecoration)
158 
159  # Select electrons only with good object quality.
160  alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronObjectQualityAlg' + self.postfix )
161  alg.selectionDecoration = 'goodOQ' + self.postfix + ',as_bits'
162  config.addPrivateTool( 'selectionTool', 'CP::EgammaIsGoodOQSelectionTool' )
163  alg.selectionTool.Mask = xAOD.EgammaParameters.BADCLUSELECTRON
164  alg.particles = config.readName (self.containerName)
165  alg.preselection = config.getPreselection (self.containerName, '')
166  config.addSelection (self.containerName, '', alg.selectionDecoration)
167 
168  if not self.splitCalibrationAndSmearing :
169  # Set up the calibration and smearing algorithm:
170  alg = self.makeCalibrationAndSmearingAlg (config, 'ElectronCalibrationAndSmearingAlg')
171  if config.isPhyslite() and not self.recalibratePhyslite :
172  alg.skipNominal = True
173  else:
174  # This splits the EgammaCalibrationAndSmearingTool into two
175  # steps. The first step applies a baseline calibration that
176  # is not affected by systematics. The second step then
177  # applies the systematics dependent corrections. The net
178  # effect is that the slower first step only has to be run
179  # once, while the second is run once per systematic.
180  #
181  # For now (22 May 24) this has to happen in the same job, as
182  # the output of the first step is not part of PHYSLITE, and
183  # even for the nominal the output of the first and second
184  # step are different. In the future the plan is to put both
185  # the output of the first and second step into PHYSLITE,
186  # allowing to skip the first step when running on PHYSLITE.
187  #
188  # WARNING: All of this is experimental, see: ATLASG-2358
189 
190  # Set up the calibration algorithm:
191  alg = self.makeCalibrationAndSmearingAlg (config, 'ElectronBaseCalibrationAlg')
192  # turn off systematics for the calibration step
193  alg.noToolSystematics = True
194  # turn off smearing for the calibration step
195  alg.calibrationAndSmearingTool.doSmearing = False
196 
197  # Set up the smearing algorithm:
198  alg = self.makeCalibrationAndSmearingAlg (config, 'ElectronCalibrationSystematicsAlg')
199  # turn off scale corrections for the smearing step
200  alg.calibrationAndSmearingTool.doScaleCorrection = False
201  alg.calibrationAndSmearingTool.useMVACalibration = False
202  alg.calibrationAndSmearingTool.decorateEmva = False
203 
204  if self.minPt > 0 :
205  # Set up the the pt selection
206  alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronPtCutAlg' + self.postfix )
207  alg.selectionDecoration = 'selectPt' + self.postfix + ',as_bits'
208  config.addPrivateTool( 'selectionTool', 'CP::AsgPtEtaSelectionTool' )
209  alg.selectionTool.minPt = self.minPt
210  alg.particles = config.readName (self.containerName)
211  alg.preselection = config.getPreselection (self.containerName, '')
212  config.addSelection (self.containerName, '', alg.selectionDecoration,
213  preselection=True)
214 
215  # Set up the isolation correction algorithm:
216  if self.isolationCorrection:
217  alg = config.createAlgorithm( 'CP::EgammaIsolationCorrectionAlg',
218  'ElectronIsolationCorrectionAlg' + self.postfix )
219  config.addPrivateTool( 'isolationCorrectionTool',
220  'CP::IsolationCorrectionTool' )
221  alg.isolationCorrectionTool.IsMC = config.dataType() is not DataType.Data
222  alg.isolationCorrectionTool.AFII_corr = (
223  0 if self.forceFullSimConfig
224  else config.dataType() is DataType.FastSim)
225  alg.egammas = config.readName (self.containerName)
226  alg.egammasOut = config.copyName (self.containerName)
227  alg.preselection = config.getPreselection (self.containerName, '')
228 
229  # Additional decorations
230  if self.writeTrackD0Z0:
231  alg = config.createAlgorithm( 'CP::AsgLeptonTrackDecorationAlg',
232  'LeptonTrackDecorator' + self.containerName + self.postfix,
233  reentrant=True )
234  alg.particles = config.readName (self.containerName)
235 
236  alg = config.createAlgorithm( 'CP::AsgEnergyDecoratorAlg', 'EnergyDecorator' + self.containerName + self.postfix )
237  alg.particles = config.readName(self.containerName)
238 
239  config.addOutputVar (self.containerName, 'pt', 'pt')
240  config.addOutputVar (self.containerName, 'eta', 'eta', noSys=True)
241  config.addOutputVar (self.containerName, 'phi', 'phi', noSys=True)
242  config.addOutputVar (self.containerName, 'e_%SYS%', 'e')
243  config.addOutputVar (self.containerName, 'charge', 'charge', noSys=True)
244 
245  if self.writeTrackD0Z0:
246  config.addOutputVar (self.containerName, 'd0_%SYS%', 'd0', noSys=True)
247  config.addOutputVar (self.containerName, 'd0sig_%SYS%', 'd0sig', noSys=True)
248  config.addOutputVar (self.containerName, 'z0sintheta_%SYS%', 'z0sintheta', noSys=True)
249  config.addOutputVar (self.containerName, 'z0sinthetasig_%SYS%', 'z0sinthetasig', noSys=True)
250 
251  # decorate truth information on the reconstructed object:
252  if self.decorateTruth and config.dataType() is not DataType.Data:
253  config.addOutputVar (self.containerName, "truthType", "truth_type", noSys=True)
254  config.addOutputVar (self.containerName, "truthOrigin", "truth_origin", noSys=True)
255 
256  config.addOutputVar (self.containerName, "firstEgMotherPdgId", "truth_firstEgMotherPdgId", noSys=True)
257  config.addOutputVar (self.containerName, "firstEgMotherTruthOrigin", "truth_firstEgMotherTruthOrigin", noSys=True)
258  config.addOutputVar (self.containerName, "firstEgMotherTruthType", "truth_firstEgMotherTruthType", noSys=True)
259 
260 
261 class ElectronWorkingPointConfig (ConfigBlock) :
262  """the ConfigBlock for the electron working point
263 
264  This may at some point be split into multiple blocks (29 Aug 22)."""
265 
266  def __init__ (self, containerName='', selectionName='') :
267  super (ElectronWorkingPointConfig, self).__init__ ()
268  self.addOption ('containerName', containerName, type=str,
269  noneAction='error',
270  info="the name of the input container.")
271  self.addOption ('selectionName', selectionName, type=str,
272  noneAction='error',
273  info="the name of the electron selection to define (e.g. tight or "
274  "loose).")
275  self.addOption ('postfix', None, type=str,
276  info="a postfix to apply to decorations and algorithm names. "
277  "Typically not needed here as selectionName is used internally.")
278  self.addOption ('trackSelection', True, type=bool,
279  info="whether or not to set up an instance of "
280  "CP::AsgLeptonTrackSelectionAlg, with the recommended d_0 and "
281  "z_0 sin(theta) cuts. The default is True.")
282  self.addOption ('maxD0Significance', 5, type=float,
283  info="maximum d0 significance used for the trackSelection"
284  "The default is 5")
285  self.addOption ('maxDeltaZ0SinTheta', 0.5, type=float,
286  info="maximum z0sinTheta in mm used for the trackSelection"
287  "The default is 0.5 mm")
288  self.addOption ('identificationWP', None, type=str,
289  info="the ID WP (string) to use. Supported ID WPs: TightLH, "
290  "MediumLH, LooseBLayerLH, TightDNN, MediumDNN, LooseDNN, "
291  "TightNoCFDNN, MediumNoCFDNN, VeryLooseNoCF97DNN.")
292  self.addOption ('isolationWP', None, type=str,
293  info="the isolation WP (string) to use. Supported isolation WPs: "
294  "HighPtCaloOnly, Loose_VarRad, Tight_VarRad, TightTrackOnly_"
295  "VarRad, TightTrackOnly_FixedRad, NonIso.")
296  self.addOption ('addSelectionToPreselection', True, type=bool,
297  info="whether to retain only electrons satisfying the working point "
298  "requirements. The default is True.")
299  self.addOption ('closeByCorrection', False, type=bool,
300  info="whether to use close-by-corrected isolation working points")
301  self.addOption ('recomputeID', False, type=bool,
302  info="whether to rerun the ID LH/DNN. The default is False, i.e. to use "
303  "derivation flags.")
304  self.addOption ('chargeIDSelectionRun2', False, type=bool,
305  info="whether to run the ECIDS tool. Only available for run 2. "
306  "The default is False.")
307  self.addOption ('recomputeChargeID', False, type=bool,
308  info="whether to rerun the ECIDS. The default is False, i.e. to use "
309  "derivation flags.")
310  self.addOption ('doFSRSelection', False, type=bool,
311  info="whether to accept additional electrons close to muons for "
312  "the purpose of FSR corrections to these muons. Expert feature "
313  "requested by the H4l analysis running on PHYSLITE. "
314  "The default is False.")
315  self.addOption ('noEffSF', False, type=bool,
316  info="disables the calculation of efficiencies and scale factors. "
317  "Experimental! only useful to test a new WP for which scale "
318  "factors are not available. The default is False.")
319  self.addOption ('saveDetailedSF', True, type=bool,
320  info="save all the independent detailed object scale factors. "
321  "The default is True.")
322  self.addOption ('saveCombinedSF', False, type=bool,
323  info="save the combined object scale factor. "
324  "The default is False.")
325  self.addOption ('forceFullSimConfig', False, type=bool,
326  info="whether to force the tool to use the configuration meant for "
327  "full simulation samples. Only for testing purposes. "
328  "The default is False.")
329  self.addOption ('correlationModelId', 'SIMPLIFIED', type=str,
330  info="the correlation model (string) to use for ID scale factors "
331  "Supported models: SIMPLIFIED (default), FULL, TOTAL, TOYS")
332  self.addOption ('correlationModelIso', 'SIMPLIFIED', type=str,
333  info="the correlation model (string) to use for isolation scale factors "
334  "Supported models: SIMPLIFIED (default), FULL, TOTAL, TOYS")
335  self.addOption ('correlationModelReco', 'SIMPLIFIED', type=str,
336  info="the correlation model (string) to use for reconstruction scale factors "
337  "Supported models: SIMPLIFIED (default), FULL, TOTAL, TOYS")
338 
339 
340  def makeAlgs (self, config) :
341 
342  log = logging.getLogger('ElectronWorkingPointConfig')
343 
344  if self.forceFullSimConfig:
345  log.warning("You are running ElectronWorkingPointConfig forcing full sim config")
346  log.warning("This is only intended to be used for testing purposes")
347 
348  selectionPostfix = self.selectionName
349  if selectionPostfix != '' and selectionPostfix[0] != '_' :
350  selectionPostfix = '_' + selectionPostfix
351 
352  # The setup below is inappropriate for Run 1
353  if config.geometry() is LHCPeriod.Run1:
354  raise ValueError ("Can't set up the ElectronWorkingPointConfig with %s, there must be something wrong!" % config.geometry().value)
355 
356  postfix = self.postfix
357  if postfix is None :
358  postfix = self.selectionName
359  if postfix != '' and postfix[0] != '_' :
360  postfix = '_' + postfix
361 
362  # Set up the track selection algorithm:
363  if self.trackSelection :
364  alg = config.createAlgorithm( 'CP::AsgLeptonTrackSelectionAlg',
365  'ElectronTrackSelectionAlg' + postfix,
366  reentrant=True )
367  alg.selectionDecoration = 'trackSelection' + postfix + ',as_bits'
368  alg.maxD0Significance = self.maxD0Significance
369  alg.maxDeltaZ0SinTheta = self.maxDeltaZ0SinTheta
370  alg.particles = config.readName (self.containerName)
371  alg.preselection = config.getPreselection (self.containerName, '')
372  if self.trackSelection :
373  config.addSelection (self.containerName, self.selectionName, alg.selectionDecoration,
374  preselection=self.addSelectionToPreselection)
375 
376  if 'LH' in self.identificationWP:
377  # Set up the likelihood ID selection algorithm
378  # It is safe to do this before calibration, as the cluster E is used
379  alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronLikelihoodAlg' + postfix )
380  alg.selectionDecoration = 'selectLikelihood' + selectionPostfix + ',as_char'
381  if self.recomputeID:
382  # Rerun the likelihood ID
383  config.addPrivateTool( 'selectionTool', 'AsgElectronLikelihoodTool' )
384  alg.selectionTool.primaryVertexContainer = 'PrimaryVertices'
385  # Here we have to match the naming convention of EGSelectorConfigurationMapping.h
386  # which differ from the one used for scale factors
387  if config.geometry() >= LHCPeriod.Run3:
388  alg.selectionTool.WorkingPoint = self.identificationWP.replace("BLayer","BL") + 'Electron'
389  elif config.geometry() is LHCPeriod.Run2:
390  alg.selectionTool.WorkingPoint = self.identificationWP.replace("BLayer","BL") + 'Electron_Run2'
391  else:
392  # Select from Derivation Framework flags
393  config.addPrivateTool( 'selectionTool', 'CP::AsgFlagSelectionTool' )
394  dfFlag = "DFCommonElectronsLH" + self.identificationWP.split('LH')[0]
395  dfFlag = dfFlag.replace("BLayer","BL")
396  alg.selectionTool.selectionFlags = [dfFlag]
397  elif 'SiHit' in self.identificationWP:
398  # Only want SiHit electrons, so veto loose LH electrons
399  algVeto = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronLikelihoodAlgVeto' + postfix + 'Veto')
400  algVeto.selectionDecoration = 'selectLikelihoodVeto' + postfix + ',as_char'
401  config.addPrivateTool( 'selectionTool', 'CP::AsgFlagSelectionTool' )
402  algVeto.selectionTool.selectionFlags = ["DFCommonElectronsLHLoose"]
403  algVeto.selectionTool.invertFlags = [True]
404  algVeto.particles = config.readName (self.containerName)
405  algVeto.preselection = config.getPreselection (self.containerName, self.selectionName)
406  # add the veto as a selection
407  config.addSelection (self.containerName, self.selectionName, algVeto.selectionDecoration,
408  preselection=self.addSelectionToPreselection)
409 
410  # Select SiHit electrons using IsEM bits
411  alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronLikelihoodAlg' + postfix )
412  alg.selectionDecoration = 'selectSiHit' + selectionPostfix + ',as_char'
413  # Select from Derivation Framework IsEM bits
414  config.addPrivateTool( 'selectionTool', 'CP::AsgMaskSelectionTool' )
415  dfVar = "DFCommonElectronsLHLooseBLIsEMValue"
416  alg.selectionTool.selectionVars = [dfVar]
417  mask = int( 0 | 0x1 << 1 | 0x1 << 2)
418  alg.selectionTool.selectionMasks = [mask]
419  elif 'DNN' in self.identificationWP:
420  if self.chargeIDSelectionRun2:
421  raise ValueError('DNN is not intended to be used with '
422  '`chargeIDSelectionRun2` option as there are '
423  'DNN WPs containing charge flip rejection.')
424  # Set up the DNN ID selection algorithm
425  alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronDNNAlg' + postfix )
426  alg.selectionDecoration = 'selectDNN' + selectionPostfix + ',as_char'
427  if self.recomputeID:
428  # Rerun the DNN ID
429  config.addPrivateTool( 'selectionTool', 'AsgElectronSelectorTool' )
430  # Here we have to match the naming convention of EGSelectorConfigurationMapping.h
431  if config.geometry() is LHCPeriod.Run3:
432  raise ValueError ( "DNN working points are not available for Run 3 yet.")
433  else:
434  alg.selectionTool.WorkingPoint = self.identificationWP + 'Electron'
435  else:
436  # Select from Derivation Framework flags
437  config.addPrivateTool( 'selectionTool', 'CP::AsgFlagSelectionTool' )
438  dfFlag = "DFCommonElectronsDNN" + self.identificationWP.split('DNN')[0]
439  alg.selectionTool.selectionFlags = [dfFlag]
440 
441  alg.particles = config.readName (self.containerName)
442  alg.preselection = config.getPreselection (self.containerName, self.selectionName)
443  config.addSelection (self.containerName, self.selectionName, alg.selectionDecoration,
444  preselection=self.addSelectionToPreselection)
445 
446  # maintain order of selections
447  if 'SiHit' in self.identificationWP:
448  # Set up the ElectronSiHitDecAlg algorithm to decorate SiHit electrons with a minimal amount of information:
449  algDec = config.createAlgorithm( 'CP::ElectronSiHitDecAlg', 'ElectronSiHitDecAlg' + postfix )
450  selDec = 'siHitEvtHasLeptonPair' + selectionPostfix + ',as_char'
451  algDec.selectionName = selDec.split(",")[0]
452  algDec.ElectronContainer = config.readName (self.containerName)
453  # Set flag to only collect SiHit electrons for events with an electron or muon pair to minimize size increase from SiHit electrons
454  algDec.RequireTwoLeptons = True
455  config.addSelection (self.containerName, self.selectionName, selDec,
456  preselection=self.addSelectionToPreselection)
457 
458  # Set up the FSR selection
459  if self.doFSRSelection :
460  # save the flag set for the WP
461  wpFlag = alg.selectionDecoration.split(",")[0]
462  alg = config.createAlgorithm( 'CP::EgammaFSRForMuonsCollectorAlg', 'EgammaFSRForMuonsCollectorAlg' + postfix )
463  alg.selectionDecoration = wpFlag
464  alg.ElectronOrPhotonContKey = config.readName (self.containerName)
465  # For SiHit electrons, set flag to remove FSR electrons.
466  # For standard electrons, FSR electrons need to be added as they may be missed by the standard selection.
467  # For SiHit electrons FSR electrons are generally always selected, so they should be removed since they will be in the standard electron container.
468  if 'SiHit' in self.identificationWP:
469  alg.vetoFSR = True
470 
471  # Set up the isolation selection algorithm:
472  if self.isolationWP != 'NonIso' :
473  alg = config.createAlgorithm( 'CP::EgammaIsolationSelectionAlg',
474  'ElectronIsolationSelectionAlg' + postfix )
475  alg.selectionDecoration = 'isolated' + selectionPostfix + ',as_char'
476  config.addPrivateTool( 'selectionTool', 'CP::IsolationSelectionTool' )
477  alg.selectionTool.ElectronWP = self.isolationWP
478  if self.closeByCorrection:
479  alg.selectionTool.IsoDecSuffix = "CloseByCorr"
480  alg.egammas = config.readName (self.containerName)
481  alg.preselection = config.getPreselection (self.containerName, self.selectionName)
482  config.addSelection (self.containerName, self.selectionName, alg.selectionDecoration,
483  preselection=self.addSelectionToPreselection)
484 
485  if self.chargeIDSelectionRun2 and config.geometry() >= LHCPeriod.Run3:
486  log.warning("ECIDS is only available for Run 2 and will not have effect in run 3.")
487 
488  # Select electrons only if they don't appear to have flipped their charge.
489  if self.chargeIDSelectionRun2 and config.geometry() < LHCPeriod.Run3:
490  alg = config.createAlgorithm( 'CP::AsgSelectionAlg',
491  'ElectronChargeIDSelectionAlg' + postfix )
492  alg.selectionDecoration = 'chargeID' + selectionPostfix + ',as_char'
493  if self.recomputeChargeID:
494  # Rerun the ECIDS BDT
495  config.addPrivateTool( 'selectionTool',
496  'AsgElectronChargeIDSelectorTool' )
497  alg.selectionTool.TrainingFile = \
498  'ElectronPhotonSelectorTools/ChargeID/ECIDS_20180731rel21Summer2018.root'
499  alg.selectionTool.WorkingPoint = 'Loose'
500  alg.selectionTool.CutOnBDT = -0.337671 # Loose 97%
501  else:
502  # Select from Derivation Framework flags
503  config.addPrivateTool( 'selectionTool', 'CP::AsgFlagSelectionTool' )
504  alg.selectionTool.selectionFlags = ["DFCommonElectronsECIDS"]
505 
506  alg.particles = config.readName (self.containerName)
507  alg.preselection = config.getPreselection (self.containerName, self.selectionName)
508  config.addSelection (self.containerName, self.selectionName, alg.selectionDecoration,
509  preselection=self.addSelectionToPreselection)
510 
511  correlationModels = ["SIMPLIFIED", "FULL", "TOTAL", "TOYS"]
512 
513  sfList = []
514  # Set up the RECO electron efficiency correction algorithm:
515  if config.dataType() is not DataType.Data and not self.noEffSF:
516 
517  if config.geometry() is LHCPeriod.Run2:
518  raise ValueError('Run 2 does not yet have efficiency correction, '
519  'please disable it by setting `noEffSF` to True.')
520  if 'DNN' in self.identificationWP:
521  raise ValueError('DNN does not yet have efficiency correction, '
522  'please disable it by setting `noEffSF` to True.')
523 
524  alg = config.createAlgorithm( 'CP::ElectronEfficiencyCorrectionAlg',
525  'ElectronEfficiencyCorrectionAlgReco' + postfix )
526  config.addPrivateTool( 'efficiencyCorrectionTool',
527  'AsgElectronEfficiencyCorrectionTool' )
528  alg.scaleFactorDecoration = 'el_reco_effSF' + selectionPostfix + '_%SYS%'
529  alg.efficiencyCorrectionTool.RecoKey = "Reconstruction"
530  if self.correlationModelReco not in correlationModels:
531  raise ValueError('Invalid correlation model for reconstruction efficiency, '
532  f'has to be one of: {", ".join(correlationModels)}')
533  if config.geometry() >= LHCPeriod.Run3 and self.correlationModelReco != "TOTAL":
534  log.warning("Only TOTAL correlation model is currently supported "
535  "for reconstruction efficiency correction in Run 3.")
536  alg.efficiencyCorrectionTool.CorrelationModel = "TOTAL"
537  else:
538  alg.efficiencyCorrectionTool.CorrelationModel = self.correlationModelReco
539  if config.dataType() is DataType.FastSim:
540  alg.efficiencyCorrectionTool.ForceDataType = (
541  PATCore.ParticleDataType.Full if self.forceFullSimConfig
542  else PATCore.ParticleDataType.Fast)
543  elif config.dataType() is DataType.FullSim:
544  alg.efficiencyCorrectionTool.ForceDataType = \
545  PATCore.ParticleDataType.Full
546  alg.outOfValidity = 2 #silent
547  alg.outOfValidityDeco = 'el_reco_bad_eff' + selectionPostfix
548  alg.electrons = config.readName (self.containerName)
549  alg.preselection = config.getPreselection (self.containerName, self.selectionName)
550  if self.saveDetailedSF:
551  config.addOutputVar (self.containerName, alg.scaleFactorDecoration,
552  'reco_effSF' + postfix)
553  sfList += [alg.scaleFactorDecoration]
554 
555  # Set up the ID electron efficiency correction algorithm:
556  if config.dataType() is not DataType.Data and not self.noEffSF:
557  alg = config.createAlgorithm( 'CP::ElectronEfficiencyCorrectionAlg',
558  'ElectronEfficiencyCorrectionAlgID' + postfix )
559  config.addPrivateTool( 'efficiencyCorrectionTool',
560  'AsgElectronEfficiencyCorrectionTool' )
561  alg.scaleFactorDecoration = 'el_id_effSF' + selectionPostfix + '_%SYS%'
562  alg.efficiencyCorrectionTool.IdKey = self.identificationWP.replace("LH","")
563  if self.correlationModelId not in correlationModels:
564  raise ValueError('Invalid correlation model for identification efficiency, '
565  f'has to be one of: {", ".join(correlationModels)}')
566  alg.efficiencyCorrectionTool.CorrelationModel = self.correlationModelId
567  if config.dataType() is DataType.FastSim:
568  alg.efficiencyCorrectionTool.ForceDataType = (
569  PATCore.ParticleDataType.Full if self.forceFullSimConfig
570  else PATCore.ParticleDataType.Fast)
571  elif config.dataType() is DataType.FullSim:
572  alg.efficiencyCorrectionTool.ForceDataType = \
573  PATCore.ParticleDataType.Full
574  alg.outOfValidity = 2 #silent
575  alg.outOfValidityDeco = 'el_id_bad_eff' + selectionPostfix
576  alg.electrons = config.readName (self.containerName)
577  alg.preselection = config.getPreselection (self.containerName, self.selectionName)
578  if self.saveDetailedSF:
579  config.addOutputVar (self.containerName, alg.scaleFactorDecoration,
580  'id_effSF' + postfix)
581  sfList += [alg.scaleFactorDecoration]
582 
583  # Set up the ISO electron efficiency correction algorithm:
584  if config.dataType() is not DataType.Data and self.isolationWP != 'NonIso' and not self.noEffSF:
585  alg = config.createAlgorithm( 'CP::ElectronEfficiencyCorrectionAlg',
586  'ElectronEfficiencyCorrectionAlgIsol' + postfix )
587  config.addPrivateTool( 'efficiencyCorrectionTool',
588  'AsgElectronEfficiencyCorrectionTool' )
589  alg.scaleFactorDecoration = 'el_isol_effSF' + selectionPostfix + '_%SYS%'
590  alg.efficiencyCorrectionTool.IdKey = self.identificationWP.replace("LH","")
591  alg.efficiencyCorrectionTool.IsoKey = self.isolationWP
592  if self.correlationModelIso not in correlationModels:
593  raise ValueError('Invalid correlation model for isolation efficiency, '
594  f'has to be one of: {", ".join(correlationModels)}')
595  if config.geometry() >= LHCPeriod.Run3:
596  log.warning("Only TOTAL correlation model is currently supported "
597  "for isolation efficiency correction in Run 3.")
598  alg.efficiencyCorrectionTool.CorrelationModel = "TOTAL"
599  else:
600  alg.efficiencyCorrectionTool.CorrelationModel = self.correlationModelIso
601  if config.dataType() is DataType.FastSim:
602  alg.efficiencyCorrectionTool.ForceDataType = (
603  PATCore.ParticleDataType.Full if self.forceFullSimConfig
604  else PATCore.ParticleDataType.Fast)
605  elif config.dataType() is DataType.FullSim:
606  alg.efficiencyCorrectionTool.ForceDataType = \
607  PATCore.ParticleDataType.Full
608  alg.outOfValidity = 2 #silent
609  alg.outOfValidityDeco = 'el_isol_bad_eff' + selectionPostfix
610  alg.electrons = config.readName (self.containerName)
611  alg.preselection = config.getPreselection (self.containerName, self.selectionName)
612  if self.saveDetailedSF:
613  config.addOutputVar (self.containerName, alg.scaleFactorDecoration,
614  'isol_effSF' + postfix)
615  sfList += [alg.scaleFactorDecoration]
616 
617  # TO-DO: add trigger SFs, for which we need ID key + ISO key + Trigger key !
618 
619  if self.chargeIDSelectionRun2:
620  # ECIDS is currently not supported in R22.
621  # SFs might become available or it will be part of the DNN ID.
622  pass
623 
624  if config.dataType() is not DataType.Data and not self.noEffSF and self.saveCombinedSF:
625  alg = config.createAlgorithm( 'CP::AsgObjectScaleFactorAlg',
626  'ElectronCombinedEfficiencyScaleFactorAlg' + postfix )
627  alg.particles = config.readName (self.containerName)
628  alg.inScaleFactors = sfList
629  alg.outScaleFactor = 'effSF' + postfix + '_%SYS%'
630  config.addOutputVar (self.containerName, alg.outScaleFactor, 'effSF' + postfix)
631 
632 
633 class ElectronTriggerAnalysisSFBlock (ConfigBlock):
634 
635  def __init__ (self, configName='') :
636  super (ElectronTriggerAnalysisSFBlock, self).__init__ ()
637 
638  self.addOption ('triggerChainsPerYear', {}, type=None,
639  info="a dictionary with key (string) the year and value (list of "
640  "strings) the trigger chains. The default is {} (empty dictionary).")
641  self.addOption ('electronID', '', type=str,
642  info="the electron ID WP (string) to use.")
643  self.addOption ('electronIsol', '', type=str,
644  info="the electron isolation WP (string) to use.")
645  self.addOption ('saveEff', False, type=bool,
646  info="define whether we decorate also the trigger scale efficiency "
647  "The default is false.")
648  self.addOption ('prefixSF', 'trigEffSF', type=str,
649  info="the decoration prefix for trigger scale factors, "
650  "the default is 'trigEffSF'")
651  self.addOption ('prefixEff', 'trigEff', type=str,
652  info="the decoration prefix for MC trigger efficiencies, "
653  "the default is 'trigEff'")
654  self.addOption ('includeAllYears', False, type=bool,
655  info="if True, all configured years will be included in all jobs. "
656  "The default is False.")
657  self.addOption ('removeHLTPrefix', True, type=bool,
658  info="remove the HLT prefix from trigger chain names, "
659  "The default is True.")
660  self.addOption ('useToolKeyAsOutput', False, type=bool,
661  info="use tool trigger key as output, "
662  "The default is False.")
663  self.addOption ('containerName', '', type=str,
664  info="the input electron container, with a possible selection, in "
665  "the format container or container.selection.")
666 
667  def makeAlgs (self, config) :
668 
669  if config.dataType() is not DataType.Data:
670  log = logging.getLogger('ElectronTriggerSFConfig')
671 
672  if self.includeAllYears and not self.useToolKeyAsOutput:
673  log.warning('`includeAllYears` is set to True, but `useToolKeyAsOutput` is set to False. '
674  'This will cause multiple branches to be written out with the same content.')
675 
676  # Dictionary from TrigGlobalEfficiencyCorrection/Triggers.cfg
677  # Key is trigger chain (w/o HLT prefix)
678  # Value is empty for single leg trigger or list of legs
679  triggerDict = TriggerDict()
680 
681  # currently recommended versions
682  version_Run2 = "2015_2018/rel21.2/Precision_Summer2020_v1"
683  version_Run3 = "2015_2025/rel22.2/2022_Summer_Prerecom_v1"
684 
685  version = version_Run2 if config.geometry() is LHCPeriod.Run2 else version_Run3
686  # Dictionary from TrigGlobalEfficiencyCorrection/MapKeys.cfg
687  # Key is year_leg
688  # Value is list of configs available, first one will be used
689  mapKeysDict = MapKeysDict(version)
690 
691  # helper function for leg filtering, very hardcoded but allows autoconfiguration
692  def filterConfFromMap(conf, electronMapKeys):
693  if not conf:
694  raise ValueError("No configuration found for trigger chain.")
695  if len(conf) == 1:
696  return conf[0]
697 
698  for c in conf:
699  if c in electronMapKeys:
700  return c
701 
702  return conf[0]
703 
704  if self.includeAllYears:
705  years = [int(year) for year in self.triggerChainsPerYear.keys()]
706  if any(year in years for year in [2015, 2016, 2017, 2018]) \
707  and any(year in years for year in [2022, 2023, 2024, 2025]):
708  raise ValueError("Mixing years from Run 2 and Run 3 in the same job is currently not supported.")
709  elif config.campaign() is Campaign.MC20a:
710  years = [2015, 2016]
711  elif config.campaign() is Campaign.MC20d:
712  years = [2017]
713  elif config.campaign() is Campaign.MC20e:
714  years = [2018]
715  elif config.campaign() in [Campaign.MC21a, Campaign.MC23a]:
716  years = [2022]
717  elif config.campaign() in [Campaign.MC23c, Campaign.MC23d]:
718  years = [2023]
719 
720  # prepare keys
721  import ROOT
722  triggerChainsPerYear_Run2 = {}
723  triggerChainsPerYear_Run3 = {}
724  for year, chains in self.triggerChainsPerYear.items():
725  if not chains:
726  log.warning("No trigger chains configured for year %s. "
727  "Assuming this is intended, no Electron trigger SF will be computed.", year)
728  continue
729 
730  chains_split = [chain.replace("HLT_", "").replace(" || ", "_OR_") for chain in chains]
731  if int(year) >= 2022:
732  triggerChainsPerYear_Run3[str(year)] = ' || '.join(chains_split)
733  else:
734  triggerChainsPerYear_Run2[str(year)] = ' || '.join(chains_split)
735  electronMapKeys_Run2 = ROOT.std.map("string", "string")()
736  electronMapKeys_Run3 = ROOT.std.map("string", "string")()
737 
738  sc_Run2 = ROOT.TrigGlobalEfficiencyCorrectionTool.suggestElectronMapKeys(triggerChainsPerYear_Run2, version_Run2, electronMapKeys_Run2)
739  sc_Run3 = ROOT.TrigGlobalEfficiencyCorrectionTool.suggestElectronMapKeys(triggerChainsPerYear_Run3, version_Run3, electronMapKeys_Run3)
740  if sc_Run2.code() != 2 or sc_Run3.code() != 2:
741  raise RuntimeError("Failed to suggest electron map keys")
742  electronMapKeys = dict(electronMapKeys_Run2) | dict(electronMapKeys_Run3)
743 
744  # collect configurations
745  triggerConfigs = {}
746  for year in years:
747  triggerChains = self.triggerChainsPerYear.get(int(year), self.triggerChainsPerYear.get(str(year), []))
748  for chain in triggerChains:
749  chain = chain.replace(" || ", "_OR_")
750  chain_noHLT = chain.replace("HLT_", "")
751  chain_out = chain_noHLT if self.removeHLTPrefix else chain
752  legs = triggerDict[chain_noHLT]
753  if not legs:
754  if chain_noHLT[0] == 'e' and chain_noHLT[1].isdigit:
755  chain_key = f"{year}_{chain_noHLT}"
756  chain_conf = mapKeysDict[chain_key][0]
757  triggerConfigs[chain_conf if self.useToolKeyAsOutput else chain_out] = chain_conf
758  else:
759  for leg in legs:
760  if leg[0] == 'e' and leg[1].isdigit:
761  leg_out = leg if self.removeHLTPrefix else f"HLT_{leg}"
762  leg_key = f"{year}_{leg}"
763  leg_conf = filterConfFromMap(mapKeysDict[leg_key], electronMapKeys)
764  triggerConfigs[leg_conf if self.useToolKeyAsOutput else leg_out] = leg_conf
765 
766  decorations = [self.prefixSF]
767  if self.saveEff:
768  decorations += [self.prefixEff]
769 
770  for label, conf in triggerConfigs.items():
771  for deco in decorations:
772  alg = config.createAlgorithm('CP::ElectronEfficiencyCorrectionAlg',
773  'EleTrigEfficiencyCorrectionsAlg' + deco +
774  '_' + label)
775  config.addPrivateTool( 'efficiencyCorrectionTool',
776  'AsgElectronEfficiencyCorrectionTool' )
777 
778  # Reproduce config from TrigGlobalEfficiencyAlg
779  alg.efficiencyCorrectionTool.MapFilePath = "ElectronEfficiencyCorrection/" + version + "/map4.txt"
780  alg.efficiencyCorrectionTool.IdKey = self.electronID.replace("LH","")
781  alg.efficiencyCorrectionTool.IsoKey = self.electronIsol
782  alg.efficiencyCorrectionTool.TriggerKey = (
783  ("Eff_" if deco == self.prefixEff else "") + conf)
784  alg.efficiencyCorrectionTool.CorrelationModel = "TOTAL"
785  alg.efficiencyCorrectionTool.ForceDataType = \
786  PATCore.ParticleDataType.Full
787 
788  alg.scaleFactorDecoration = f"el_{deco}_{label}_%SYS%"
789 
790  alg.outOfValidity = 2 #silent
791  alg.outOfValidityDeco = f"bad_eff_ele{deco}_{label}"
792  alg.electrons = config.readName (self.containerName)
793  alg.preselection = config.getPreselection (self.containerName, "")
794  config.addOutputVar (self.containerName, alg.scaleFactorDecoration, f"{deco}_{label}")
795 
796 
797 class ElectronLRTMergedConfig (ConfigBlock) :
798  def __init__ (self) :
799  super (ElectronLRTMergedConfig, self).__init__ ()
800  self.addOption (
801  'inputElectrons', 'Electrons', type=str,
802  noneAction='error',
803  info="the name of the input electron container."
804  )
805  self.addOption (
806  'inputLRTElectrons', 'LRTElectrons', type=str,
807  noneAction='error',
808  info="the name of the input LRT electron container."
809  )
810  self.addOption (
811  'containerName', 'Electrons_LRTMerged', type=str,
812  noneAction='error',
813  info="the name of the output container after LRT merging."
814  )
815 
816 
817  def makeAlgs (self, config) :
818 
819  if config.isPhyslite() :
820  raise(RuntimeError("Electron LRT merging is not available in Physlite mode"))
821 
822  alg = config.createAlgorithm( "CP::ElectronLRTMergingAlg", "ElectronLRTMergingAlg" + self.containerName )
823  alg.PromptElectronLocation = self.inputElectrons
824  alg.LRTElectronLocation = self.inputLRTElectrons
825  alg.OutputCollectionName = self.containerName
826  alg.CreateViewCollection = False
replace
std::string replace(std::string s, const std::string &s2, const std::string &s3)
Definition: hcg.cxx:307
SystemOfUnits
PATCore::ParticleDataType
Definition: PATCoreEnums.h:21
python.ElectronAnalysisConfig.ElectronTriggerAnalysisSFBlock.__init__
def __init__(self, configName='')
Definition: ElectronAnalysisConfig.py:635
python.ElectronAnalysisConfig.ElectronWorkingPointConfig.makeAlgs
def makeAlgs(self, config)
Definition: ElectronAnalysisConfig.py:340
TriggerLeg_DictHelpers.MapKeysDict
def MapKeysDict(target_version)
Definition: TriggerLeg_DictHelpers.py:37
python.ElectronAnalysisConfig.ElectronCalibrationConfig
Definition: ElectronAnalysisConfig.py:18
python.ElectronAnalysisConfig.ElectronWorkingPointConfig.__init__
def __init__(self, containerName='', selectionName='')
Definition: ElectronAnalysisConfig.py:266
python.ElectronAnalysisConfig.ElectronLRTMergedConfig.makeAlgs
def makeAlgs(self, config)
Definition: ElectronAnalysisConfig.py:817
python.ElectronAnalysisConfig.ElectronCalibrationConfig.makeCalibrationAndSmearingAlg
def makeCalibrationAndSmearingAlg(self, config, name)
Definition: ElectronAnalysisConfig.py:79
python.ElectronAnalysisConfig.ElectronTriggerAnalysisSFBlock.makeAlgs
def makeAlgs(self, config)
Definition: ElectronAnalysisConfig.py:667
python.ElectronAnalysisConfig.ElectronLRTMergedConfig
Definition: ElectronAnalysisConfig.py:797
python.ElectronAnalysisConfig.ElectronLRTMergedConfig.__init__
def __init__(self)
Definition: ElectronAnalysisConfig.py:798
python.LArMinBiasAlgConfig.int
int
Definition: LArMinBiasAlgConfig.py:59
python.ElectronAnalysisConfig.ElectronTriggerAnalysisSFBlock
Definition: ElectronAnalysisConfig.py:633
python.ElectronAnalysisConfig.ElectronCalibrationConfig.__init__
def __init__(self, containerName='')
Definition: ElectronAnalysisConfig.py:21
TCS::join
std::string join(const std::vector< std::string > &v, const char c=',')
Definition: Trigger/TrigT1/L1Topo/L1TopoCommon/Root/StringUtils.cxx:10
python.ElectronAnalysisConfig.ElectronWorkingPointConfig
Definition: ElectronAnalysisConfig.py:261
python.ElectronAnalysisConfig.ElectronCalibrationConfig.makeAlgs
def makeAlgs(self, config)
Definition: ElectronAnalysisConfig.py:116
TriggerLeg_DictHelpers.TriggerDict
def TriggerDict()
Definition: TriggerLeg_DictHelpers.py:8
TrigJetMonitorAlgorithm.items
items
Definition: TrigJetMonitorAlgorithm.py:71
get
T * get(TKey *tobj)
get a TObject* from a TKey* (why can't a TObject be a TKey?)
Definition: hcg.cxx:127
str
Definition: BTagTrackIpAccessor.cxx:11
python.Bindings.keys
keys
Definition: Control/AthenaPython/python/Bindings.py:798
Trk::split
@ split
Definition: LayerMaterialProperties.h:38