ATLAS Offline Software
Loading...
Searching...
No Matches
ElectronAnalysisConfig.py
Go to the documentation of this file.
1# Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
2
3# AnaAlgorithm import(s):
4from AnalysisAlgorithmsConfig.ConfigBlock import ConfigBlock
5from AnalysisAlgorithmsConfig.ConfigSequence import groupBlocks
6from AthenaCommon.SystemOfUnits import GeV
7from AthenaConfiguration.Enums import LHCPeriod
8from AnalysisAlgorithmsConfig.ConfigAccumulator import DataType
9from TrackingAnalysisAlgorithms.TrackingAnalysisConfig import InDetTrackCalibrationConfig
10from TrigGlobalEfficiencyCorrection.TriggerLeg_DictHelpers import TriggerDict, MapKeysDict
11from AthenaCommon.Logging import logging
12
13# E/gamma import(s).
14from xAODEgamma.xAODEgammaParameters import xAOD
15
17
18
19class ElectronCalibrationConfig (ConfigBlock) :
20 """the ConfigBlock for the electron four-momentum correction"""
21
22 def __init__ (self) :
23 super (ElectronCalibrationConfig, self).__init__ ()
24 self.setBlockName('Electrons')
25 self.addOption ('inputContainer', '', type=str,
26 info="the name of the input electron container. If left empty, automatically defaults "
27 "to `AnalysisElectrons` for PHYSLITE or `Electrons` otherwise.")
28 self.addOption ('containerName', '', type=str,
29 noneAction='error',
30 info="the name of the output container after calibration.")
31 self.addOption ('ESModel', '', type=str,
32 info="flag for Egamma calibration. If left empty, use the current recommendations.")
33 self.addOption ('decorrelationModel', '1NP_v1', type=str,
34 info="decorrelation model for the EGamma energy scale. Supported choices are: `1NP_v1`, `FULL_v1`.")
35 self.addOption ('postfix', '', type=str,
36 info="a postfix to apply to decorations and algorithm names. Typically "
37 "not needed here since the calibration is common to all electrons.")
38 self.addOption ('crackVeto', False, type=bool,
39 info=r"whether to perform LAr crack veto based on the cluster $\eta$, "
40 r"i.e. remove electrons within $1.37<\vert\eta\vert<1.52$.")
41 self.addOption ('isolationCorrection', True, 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 expertMode=True)
46 self.addOption ('recalibratePhyslite', True, type=bool,
47 info="whether to run the `CP::EgammaCalibrationAndSmearingAlg` on "
48 "PHYSLITE derivations.")
49 self.addOption ('minPt', 4.5*GeV, type=float,
50 info=r"the minimum $p_\mathrm{T}$ cut (in MeV) to apply to calibrated electrons.")
51 self.addOption ('maxEta', 2.47, type=float,
52 info=r"maximum electron $\vert\eta\vert$.")
53 self.addOption ('forceFullSimConfigForP4', False, type=bool,
54 info="whether to force the tool to use the configuration meant for "
55 "full simulation samples for 4-vector corrections. Only for testing purposes.")
56 self.addOption ('forceFullSimConfigForIso', False, type=bool,
57 info="whether to force the tool to use the configuration meant for "
58 "full simulation samples for isolation corrections. Only for testing purposes.")
59 self.addOption ('splitCalibrationAndSmearing', False, type=bool,
60 info="EXPERIMENTAL: This splits the `EgammaCalibrationAndSmearingTool` "
61 " into two steps. The first step applies a baseline calibration that "
62 "is not affected by systematics. The second step then applies the "
63 "systematics-dependent corrections. The net effect is that the "
64 "slower first step only has to be run once, while the second is run "
65 "once per systematic. ATLASG-2358.",
66 expertMode=True)
67 self.addOption ('runTrackBiasing', False, type=bool,
68 info="EXPERIMENTAL: This enables the `InDetTrackBiasingTool`, for "
69 "tracks associated to electrons. The tool does not have Run 3 "
70 "recommendations yet.",
71 expertMode=True)
72 self.addOption ('decorateTruth', False, type=bool,
73 info="decorate truth particle information on the reconstructed one.")
74 self.addOption ('decorateCaloClusterEta', False, type=bool,
75 info=r"decorate the calo cluster $\eta$.")
76 self.addOption ('writeTrackD0Z0', False, type = bool,
77 info=r"save the $d_0$ significance and $z_0\sin\theta$ variables.")
78 self.addOption ('decorateEmva', False, type=bool,
79 info="decorate `E_mva_only` on the electrons (needed for columnar tools/PHYSLITE).")
80 self.addOption ('decorateSamplingPattern', False, type=bool,
81 info="decorate `samplingPattern` on the clusters (meant for PHYSLITE).")
82 self.addOption ('addGlobalFELinksDep', False, type=bool,
83 info="whether to add dependencies for the global FE links (needed for PHYSLITE production)",
84 expertMode=True)
85
86 def instanceName (self) :
87 """Return the instance name for this block"""
88 return self.containerName + self.postfix
89
90 def makeCalibrationAndSmearingAlg (self, config, name) :
91 """Create the calibration and smearing algorithm
92
93 Factoring this out into its own function, as we want to
94 instantiate it in multiple places"""
95 log = logging.getLogger('ElectronCalibrationConfig')
96
97 # Set up the calibration and smearing algorithm:
98 alg = config.createAlgorithm( 'CP::EgammaCalibrationAndSmearingAlg', name )
99 config.addPrivateTool( 'calibrationAndSmearingTool',
100 'CP::EgammaCalibrationAndSmearingTool' )
101 # Set default ESModel per period
102 if self.ESModel:
103 alg.calibrationAndSmearingTool.ESModel = self.ESModel
104 else:
105 if config.geometry() is LHCPeriod.Run2:
106 alg.calibrationAndSmearingTool.ESModel = 'es2023_R22_Run2_v1'
107 elif config.geometry() is LHCPeriod.Run3:
108 alg.calibrationAndSmearingTool.ESModel = 'es2024_Run3_v0'
109 elif config.geometry() is LHCPeriod.Run4:
110 log.warning("No ESModel set for Run4, using Run 3 model instead")
111 alg.calibrationAndSmearingTool.ESModel = 'es2024_Run3_v0'
112 else:
113 raise ValueError (f"Can't set up the ElectronCalibrationConfig with {config.geometry().value}, "
114 "there must be something wrong!")
115
116 alg.calibrationAndSmearingTool.decorrelationModel = self.decorrelationModel
117 alg.calibrationAndSmearingTool.useFastSim = (
119 else int( config.dataType() is DataType.FastSim ))
120 alg.calibrationAndSmearingTool.decorateEmva = self.decorateEmva
121 alg.egammas = config.readName (self.containerName)
122 alg.egammasOut = config.copyName (self.containerName)
123 alg.preselection = config.getPreselection (self.containerName, '')
124 return alg
125
126
127 def makeAlgs (self, config) :
128
129 log = logging.getLogger('ElectronCalibrationConfig')
130
132 log.warning("You are running ElectronCalibrationConfig forcing full sim config")
133 log.warning(" This is only intended to be used for testing purposes")
134
135 inputContainer = "AnalysisElectrons" if config.isPhyslite() else "Electrons"
137 inputContainer = self.inputContainer
138 config.setSourceName (self.containerName, inputContainer)
139
140 # Decorate calo cluster eta if required
142 alg = config.createAlgorithm( 'CP::EgammaCaloClusterEtaAlg',
143 'ElectronEgammaCaloClusterEtaAlg',
144 reentrant=True )
145 alg.particles = config.readName(self.containerName)
146 config.addOutputVar (self.containerName, 'caloEta2', 'caloEta2', noSys=True)
147
149 config.createAlgorithm( 'CP::EgammaSamplingPatternDecoratorAlg', 'EgammaSamplingPatternDecoratorAlg' )
150
151 # Set up a shallow copy to decorate
152 if config.wantCopy (self.containerName) :
153 alg = config.createAlgorithm( 'CP::AsgShallowCopyAlg', 'ElectronShallowCopyAlg' )
154 alg.input = config.readName (self.containerName)
155 alg.output = config.copyName (self.containerName)
156 alg.outputType = 'xAOD::ElectronContainer'
157 decorationList = ['DFCommonElectronsLHLoose','ptcone20_Nonprompt_All_MaxWeightTTVALooseCone_pt1000_CloseByCorr',
158 'ptvarcone30_Nonprompt_All_MaxWeightTTVALooseCone_pt1000_CloseByCorr',
159 'topoetcone20_CloseByCorr','DFCommonAddAmbiguity']
161 decorationList += ['neutralGlobalFELinks', 'chargedGlobalFELinks']
162 if config.dataType() is not DataType.Data:
163 decorationList += ['TruthLink']
164 alg.declareDecorations = decorationList
165
166 # Set up the eta-cut on all electrons prior to everything else
167 alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronEtaCutAlg' )
168 alg.selectionDecoration = 'selectEta' + self.postfix + ',as_bits'
169 config.addPrivateTool( 'selectionTool', 'CP::AsgPtEtaSelectionTool' )
170 alg.selectionTool.maxEta = self.maxEta
171 if self.crackVeto:
172 alg.selectionTool.etaGapLow = 1.37
173 alg.selectionTool.etaGapHigh = 1.52
174 alg.selectionTool.useClusterEta = True
175 alg.particles = config.readName (self.containerName)
176 alg.preselection = config.getPreselection (self.containerName, '')
177 config.addSelection (self.containerName, '', alg.selectionDecoration)
178
179 # Select electrons only with good object quality.
180 alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronObjectQualityAlg' )
181 config.setExtraInputs ({('xAOD::EventInfo', 'EventInfo.RandomRunNumber')})
182 alg.selectionDecoration = 'goodOQ' + self.postfix + ',as_bits'
183 config.addPrivateTool( 'selectionTool', 'CP::EgammaIsGoodOQSelectionTool' )
184 alg.selectionTool.Mask = xAOD.EgammaParameters.BADCLUSELECTRON
185 alg.particles = config.readName (self.containerName)
186 alg.preselection = config.getPreselection (self.containerName, '')
187 config.addSelection (self.containerName, '', alg.selectionDecoration)
188
190 # Set up the calibration and smearing algorithm:
191 alg = self.makeCalibrationAndSmearingAlg (config, 'ElectronCalibrationAndSmearingAlg')
192 if config.isPhyslite() and not self.recalibratePhyslite :
193 alg.skipNominal = True
194 else:
195 # This splits the EgammaCalibrationAndSmearingTool into two
196 # steps. The first step applies a baseline calibration that
197 # is not affected by systematics. The second step then
198 # applies the systematics dependent corrections. The net
199 # effect is that the slower first step only has to be run
200 # once, while the second is run once per systematic.
201 #
202 # For now (22 May 24) this has to happen in the same job, as
203 # the output of the first step is not part of PHYSLITE, and
204 # even for the nominal the output of the first and second
205 # step are different. In the future the plan is to put both
206 # the output of the first and second step into PHYSLITE,
207 # allowing to skip the first step when running on PHYSLITE.
208 #
209 # WARNING: All of this is experimental, see: ATLASG-2358
210
211 # Set up the calibration algorithm:
212 alg = self.makeCalibrationAndSmearingAlg (config, 'ElectronBaseCalibrationAlg')
213 # turn off systematics for the calibration step
214 alg.noToolSystematics = True
215 # turn off smearing for the calibration step
216 alg.calibrationAndSmearingTool.doSmearing = False
217
218 # Set up the smearing algorithm:
219 alg = self.makeCalibrationAndSmearingAlg (config, 'ElectronCalibrationSystematicsAlg')
220 # turn off scale corrections for the smearing step
221 alg.calibrationAndSmearingTool.doScaleCorrection = False
222 alg.calibrationAndSmearingTool.useMVACalibration = False
223 alg.calibrationAndSmearingTool.decorateEmva = False
224
225 if self.minPt > 0 :
226 # Set up the the pt selection
227 alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronPtCutAlg' )
228 alg.selectionDecoration = 'selectPt' + self.postfix + ',as_bits'
229 config.addPrivateTool( 'selectionTool', 'CP::AsgPtEtaSelectionTool' )
230 alg.selectionTool.minPt = self.minPt
231 alg.particles = config.readName (self.containerName)
232 alg.preselection = config.getPreselection (self.containerName, '')
233 config.addSelection (self.containerName, '', alg.selectionDecoration,
234 preselection=True)
235
236 # Set up the isolation correction algorithm:
238 alg = config.createAlgorithm( 'CP::EgammaIsolationCorrectionAlg',
239 'ElectronIsolationCorrectionAlg' )
240 config.addPrivateTool( 'isolationCorrectionTool',
241 'CP::IsolationCorrectionTool' )
242 alg.isolationCorrectionTool.IsMC = config.dataType() is not DataType.Data
243 alg.isolationCorrectionTool.AFII_corr = (
244 0 if self.forceFullSimConfigForIso
245 else config.dataType() is DataType.FastSim)
246 alg.isolationCorrectionTool.FixTimingIssueInCore = True
247 alg.isolationCorrectionTool.ToolVer = "REL22"
248 alg.isolationCorrectionTool.CorrFile = "IsolationCorrections/v6/isolation_ptcorrections_rel22_mc20.root"
249 alg.egammas = config.readName (self.containerName)
250 alg.egammasOut = config.copyName (self.containerName)
251 alg.egammasType = 'xAOD::ElectronContainer'
252 alg.preselection = config.getPreselection (self.containerName, '')
253 else:
254 log.warning("You are not applying the isolation corrections")
255 log.warning("This is only intended to be used for testing purposes")
256
257 # Additional decorations
259 alg = config.createAlgorithm( 'CP::AsgLeptonTrackDecorationAlg',
260 'LeptonTrackDecorator' )
261 if config.dataType() is not DataType.Data:
263 InDetTrackCalibrationConfig.makeTrackBiasingTool(config, alg)
264 InDetTrackCalibrationConfig.makeTrackSmearingTool(config, alg)
265 alg.particles = config.readName (self.containerName)
266
267 alg = config.createAlgorithm( 'CP::AsgEnergyDecoratorAlg', 'EnergyDecorator' )
268 alg.particles = config.readName(self.containerName)
269
270 config.addOutputVar (self.containerName, 'pt', 'pt')
271 config.addOutputVar (self.containerName, 'eta', 'eta', noSys=True)
272 config.addOutputVar (self.containerName, 'phi', 'phi', noSys=True)
273 config.addOutputVar (self.containerName, 'e_%SYS%', 'e')
274 config.addOutputVar (self.containerName, 'charge', 'charge', noSys=True)
275 config.addOutputVar (self.containerName, 'caloClusterEnergyReso_%SYS%', 'caloClusterEnergyReso', noSys=True)
276
277 if self.writeTrackD0Z0:
278 config.addOutputVar (self.containerName, 'd0_%SYS%', 'd0')
279 config.addOutputVar (self.containerName, 'd0sig_%SYS%', 'd0sig')
280 config.addOutputVar (self.containerName, 'z0_%SYS%', 'z0')
281 config.addOutputVar (self.containerName, 'z0sintheta_%SYS%', 'z0sintheta')
282 config.addOutputVar (self.containerName, 'z0sinthetasig_%SYS%', 'z0sinthetasig')
283
284 # decorate truth information on the reconstructed object:
285 if self.decorateTruth and config.dataType() is not DataType.Data:
286 config.addOutputVar (self.containerName, "truthType", "truth_type", noSys=True, auxType='int')
287 config.addOutputVar (self.containerName, "truthOrigin", "truth_origin", noSys=True, auxType='int')
288
289 config.addOutputVar (self.containerName, "firstEgMotherPdgId", "truth_firstEgMotherPdgId", noSys=True, auxType='int')
290 config.addOutputVar (self.containerName, "firstEgMotherTruthOrigin", "truth_firstEgMotherTruthOrigin", noSys=True, auxType='int')
291 config.addOutputVar (self.containerName, "firstEgMotherTruthType", "truth_firstEgMotherTruthType", noSys=True, auxType='int')
292
293
295 """the ConfigBlock for the electron working point selection"""
296
297 def __init__ (self) :
298 super (ElectronWorkingPointSelectionConfig, self).__init__ ()
299 self.setBlockName('ElectronWorkingPointSelection')
300 self.addOption ('containerName', '', type=str,
301 noneAction='error',
302 info="the name of the input container.")
303 self.addOption ('selectionName', '', type=str,
304 noneAction='error',
305 info="the name of the electron selection to define (e.g. `tight` or "
306 "`loose`).")
307 self.addOption ('postfix', None, type=str,
308 info="a postfix to apply to decorations and algorithm names. "
309 "Typically not needed here as `selectionName` is used internally.")
310 self.addOption ('trackSelection', True, type=bool,
311 info="whether or not to set up an instance of "
312 "`CP::AsgLeptonTrackSelectionAlg`, with the recommended $d_0$ and "
313 r"$z_0\sin\theta$ cuts.")
314 self.addOption ('maxD0Significance', 5, type=float,
315 info="maximum $d_0$ significance used for the track selection.")
316 self.addOption ('maxDeltaZ0SinTheta', 0.5, type=float,
317 info=r"maximum $z_0\sin\theta$ (in mm) used for the track selection.")
318 self.addOption ('identificationWP', None, type=str,
319 info="the ID WP to use. Supported ID WPs: `TightLH`, "
320 "`MediumLH`, `LooseBLayerLH`, `TightDNN`, `MediumDNN`, `LooseDNN`, "
321 "`TightNoCFDNN`, `MediumNoCFDNN`, `VeryLooseNoCF97DNN`, `NoID`.",
322 expertMode=["NoID"])
323 self.addOption ('isolationWP', None, type=str,
324 info="the isolation WP to use. Supported isolation WPs: "
325 "`HighPtCaloOnly`, `Loose_VarRad`, `Tight_VarRad`, `TightTrackOnly_"
326 "VarRad`, `TightTrackOnly_FixedRad`, `NonIso`.")
327 self.addOption ('convSelection', None, type=str,
328 info="enter additional selection to use for conversions. To be used with "
329 "`TightLH` or will crash. Supported keywords:"
330 "`Veto`, `MatConv`, `GammaStar`.")
331 self.addOption ('addSelectionToPreselection', True, type=bool,
332 info="whether to retain only electrons satisfying the working point "
333 "requirements.")
334 self.addOption ('closeByCorrection', False, type=bool,
335 info="whether to use close-by-corrected isolation working points.")
336 self.addOption ('recomputeID', False, type=bool,
337 info="whether to rerun the ID LH/DNN, or rely on derivation flags.")
338 self.addOption ('chargeIDSelectionRun2', False, type=bool,
339 info="whether to run the ECIDS tool. Only available for Run 2.")
340 self.addOption ('recomputeChargeID', False, type=bool,
341 info="whether to rerun the ECIDS, or rely on derivation flags.")
342 self.addOption ('doFSRSelection', False, type=bool,
343 info="whether to accept additional electrons close to muons for "
344 "the purpose of FSR corrections to these muons. Expert feature "
345 "requested by the H4l analysis running on PHYSLITE.",
346 expertMode=True)
347 self.addOption ('muonsForFSRSelection', None, type=str,
348 info="the name of the muon container to use for the FSR selection. "
349 "If not specified, AnalysisMuons is used.",
350 expertMode=True)
351 self.addOption ('mainElectronContainer', None, type=str,
352 info="the name of the main electron container to use for the SiHit selection. "
353 "If not specified, this defaults to AnalysisElectrons.",
354 expertMode=True)
355
356 def instanceName (self) :
357 """Return the instance name for this block"""
358 if self.postfix is not None :
359 return self.containerName + '_' + self.selectionName + self.postfix
360 return self.containerName + '_' + self.selectionName
361
362 def makeAlgs (self, config) :
363
364 log = logging.getLogger('ElectronWorkingPointSelectionConfig')
365
366 selectionPostfix = self.selectionName
367 if selectionPostfix != '' and selectionPostfix[0] != '_' :
368 selectionPostfix = '_' + selectionPostfix
369
370 # The setup below is inappropriate for Run 1
371 if config.geometry() is LHCPeriod.Run1:
372 raise ValueError ("Can't set up the ElectronWorkingPointSelectionConfig with %s, there must be something wrong!" % config.geometry().value)
373
374 postfix = self.postfix
375 if postfix is None :
376 postfix = self.selectionName
377 if postfix != '' and postfix[0] != '_' :
378 postfix = '_' + postfix
379
380 # Set up the track selection algorithm:
382 alg = config.createAlgorithm( 'CP::AsgLeptonTrackSelectionAlg',
383 'ElectronTrackSelectionAlg',
384 reentrant=True )
385 alg.selectionDecoration = 'trackSelection' + postfix + ',as_bits'
386 alg.maxD0Significance = self.maxD0Significance
387 alg.maxDeltaZ0SinTheta = self.maxDeltaZ0SinTheta
388 alg.particles = config.readName (self.containerName)
389 alg.preselection = config.getPreselection (self.containerName, '')
390 if self.trackSelection :
391 config.addSelection (self.containerName, self.selectionName, alg.selectionDecoration,
392 preselection=self.addSelectionToPreselection)
393
394 if 'LH' in self.identificationWP:
395 # Set up the likelihood ID selection algorithm
396 # It is safe to do this before calibration, as the cluster E is used
397 alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronLikelihoodAlg' )
398 alg.selectionDecoration = 'selectLikelihood' + selectionPostfix + ',as_char'
399 if self.recomputeID:
400 # Rerun the likelihood ID
401 config.addPrivateTool( 'selectionTool', 'AsgElectronLikelihoodTool' )
402 alg.selectionTool.primaryVertexContainer = 'PrimaryVertices'
403 # Here we have to match the naming convention of EGSelectorConfigurationMapping.h
404 # which differ from the one used for scale factors
405 if config.geometry() >= LHCPeriod.Run3:
406 if 'HI' not in self.identificationWP:
407 alg.selectionTool.WorkingPoint = self.identificationWP.replace("BLayer","BL") + 'Electron'
408 else:
409 alg.selectionTool.WorkingPoint = self.identificationWP.replace('_HI', 'Electron_HI')
410 elif config.geometry() is LHCPeriod.Run2:
411 alg.selectionTool.WorkingPoint = self.identificationWP.replace("BLayer","BL") + 'Electron_Run2'
412 else:
413 # Select from Derivation Framework flags
414 config.addPrivateTool( 'selectionTool', 'CP::AsgFlagSelectionTool' )
415 dfFlag = "DFCommonElectronsLH" + self.identificationWP.split('LH')[0]
416 dfFlag = dfFlag.replace("BLayer","BL")
417 alg.selectionTool.selectionFlags = [dfFlag]
418 elif 'SiHit' in self.identificationWP:
419 # Only want SiHit electrons, so veto loose LH electrons
420 algVeto = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronLikelihoodAlgVeto')
421 algVeto.selectionDecoration = 'selectLikelihoodVeto' + postfix + ',as_char'
422 config.addPrivateTool( 'selectionTool', 'CP::AsgFlagSelectionTool' )
423 algVeto.selectionTool.selectionFlags = ["DFCommonElectronsLHLoose"]
424 algVeto.selectionTool.invertFlags = [True]
425 algVeto.particles = config.readName (self.containerName)
426 algVeto.preselection = config.getPreselection (self.containerName, self.selectionName)
427 # add the veto as a selection
428 config.addSelection (self.containerName, self.selectionName, algVeto.selectionDecoration,
429 preselection=self.addSelectionToPreselection)
430
431 # Select SiHit electrons using IsEM bits
432 alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronLikelihoodAlg' )
433 alg.selectionDecoration = 'selectSiHit' + selectionPostfix + ',as_char'
434 # Select from Derivation Framework IsEM bits
435 config.addPrivateTool( 'selectionTool', 'CP::AsgMaskSelectionTool' )
436 dfVar = "DFCommonElectronsLHLooseBLIsEMValue"
437 alg.selectionTool.selectionVars = [dfVar]
438 mask = int( 0 | 0x1 << 1 | 0x1 << 2)
439 alg.selectionTool.selectionMasks = [mask]
440 elif 'DNN' in self.identificationWP:
442 raise ValueError('DNN is not intended to be used with '
443 '`chargeIDSelectionRun2` option as there are '
444 'DNN WPs containing charge flip rejection.')
445 # Set up the DNN ID selection algorithm
446 alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronDNNAlg' )
447 alg.selectionDecoration = 'selectDNN' + selectionPostfix + ',as_char'
448 if self.recomputeID:
449 # Rerun the DNN ID
450 config.addPrivateTool( 'selectionTool', 'AsgElectronSelectorTool' )
451 # Here we have to match the naming convention of EGSelectorConfigurationMapping.h
452 if config.geometry() is LHCPeriod.Run3:
453 raise ValueError ( "DNN working points are not available for Run 3 yet.")
454 else:
455 alg.selectionTool.WorkingPoint = self.identificationWP + 'Electron'
456 else:
457 # Select from Derivation Framework flags
458 config.addPrivateTool( 'selectionTool', 'CP::AsgFlagSelectionTool' )
459 dfFlag = "DFCommonElectronsDNN" + self.identificationWP.split('DNN')[0]
460 alg.selectionTool.selectionFlags = [dfFlag]
461 elif self.identificationWP == 'NoID':
462 alg = None
463 else:
464 raise ValueError (f"Electron ID working point '{self.identificationWP}' is not recognised!")
465
466 if alg is not None:
467 alg.particles = config.readName (self.containerName)
468 alg.preselection = config.getPreselection (self.containerName, self.selectionName)
469 config.addSelection (self.containerName, self.selectionName, alg.selectionDecoration,
470 preselection=self.addSelectionToPreselection)
471
472 # maintain order of selections
473 if 'SiHit' in self.identificationWP:
474 # Set up the ElectronSiHitDecAlg algorithm to decorate SiHit electrons with a minimal amount of information:
475 algDec = config.createAlgorithm( 'CP::ElectronSiHitDecAlg', 'ElectronSiHitDecAlg' )
476 selDec = 'siHitEvtHasLeptonPair' + selectionPostfix + ',as_char'
477 algDec.selectionName = selDec.split(",")[0]
478 algDec.ElectronContainer = config.readName (self.containerName)
479 if self.muonsForFSRSelection is not None:
480 algDec.AnalMuonContKey = config.readName (self.muonsForFSRSelection)
481 if self.mainElectronContainer is not None:
482 algDec.AnalElectronContKey = config.readName (self.mainElectronContainer)
483 # Set flag to only collect SiHit electrons for events with an electron or muon pair to minimize size increase from SiHit electrons
484 algDec.RequireTwoLeptons = True
485 config.addSelection (self.containerName, self.selectionName, selDec,
486 preselection=self.addSelectionToPreselection)
487
488 # Additional selection for conversions and gamma*
489 if self.convSelection is not None:
490 # skip if not applied together with TightLH
491 if self.identificationWP != 'TightLH':
492 raise ValueError(f"convSelection can only be used with TightLH ID, "
493 f"whereas {self.identificationWP} has been selected. convSelection option will be ignored.")
494 # check if allowed value
495 allowedValues = ["Veto", "GammaStar", "MatConv"]
496 if self.convSelection not in allowedValues:
497 raise ValueError(f"convSelection has been set to {self.convSelection}, which is not a valid option. "
498 f"convSelection option must be one of {allowedValues}.")
499
500 # ambiguityType == 0
501 alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronAmbiguityTypeAlg' )
502 alg.selectionDecoration = 'selectAmbiguityType' + selectionPostfix + ',as_char'
503 config.addPrivateTool( 'selectionTool', 'CP::AsgNumDecorationSelectionToolUInt8' )
504 alg.selectionTool.decorationName = "ambiguityType"
505 alg.selectionTool.doEqual = True
506 alg.selectionTool.equal = 0
507 alg.particles = config.readName (self.containerName)
508 alg.preselection = config.getPreselection (self.containerName, self.selectionName)
509 config.addSelection (self.containerName, self.selectionName, alg.selectionDecoration,
510 preselection=self.addSelectionToPreselection)
511
512 # DFCommonAddAmbiguity selection
513 alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronDFCommonAddAmbiguityAlg' )
514 alg.selectionDecoration = 'selectDFCommonAddAmbiguity' + selectionPostfix + ',as_char'
515 config.addPrivateTool( 'selectionTool', 'CP::AsgNumDecorationSelectionToolInt' )
516 alg.selectionTool.decorationName = "DFCommonAddAmbiguity"
517 if self.convSelection == "Veto":
518 alg.selectionTool.doMax = True
519 alg.selectionTool.max = 1
520 elif self.convSelection == "GammaStar":
521 alg.selectionTool.doEqual = True
522 alg.selectionTool.equal = 1
523 elif self.convSelection == "MatConv":
524 alg.selectionTool.doEqual = True
525 alg.selectionTool.equal = 2
526 alg.particles = config.readName (self.containerName)
527 alg.preselection = config.getPreselection (self.containerName, self.selectionName)
528 config.addSelection (self.containerName, self.selectionName, alg.selectionDecoration,
529 preselection=self.addSelectionToPreselection)
530
531 # Set up the FSR selection
533 # save the flag set for the WP
534 wpFlag = alg.selectionDecoration.split(",")[0]
535 alg = config.createAlgorithm( 'CP::EgammaFSRForMuonsCollectorAlg', 'EgammaFSRForMuonsCollectorAlg' )
536 alg.selectionDecoration = wpFlag
537 alg.ElectronOrPhotonContKey = config.readName (self.containerName)
538 if self.muonsForFSRSelection is not None:
539 alg.MuonContKey = config.readName (self.muonsForFSRSelection)
540 # For SiHit electrons, set flag to remove FSR electrons.
541 # For standard electrons, FSR electrons need to be added as they may be missed by the standard selection.
542 # For SiHit electrons FSR electrons are generally always selected, so they should be removed since they will be in the standard electron container.
543 if 'SiHit' in self.identificationWP:
544 alg.vetoFSR = True
545
546 # Set up the isolation selection algorithm:
547 if self.isolationWP != 'NonIso' :
548 alg = config.createAlgorithm( 'CP::EgammaIsolationSelectionAlg',
549 'ElectronIsolationSelectionAlg' )
550 alg.selectionDecoration = 'isolated' + selectionPostfix + ',as_char'
551 config.addPrivateTool( 'selectionTool', 'CP::IsolationSelectionTool' )
552 alg.selectionTool.ElectronWP = self.isolationWP
554 alg.selectionTool.IsoDecSuffix = "CloseByCorr"
555 alg.egammas = config.readName (self.containerName)
556 alg.preselection = config.getPreselection (self.containerName, self.selectionName)
557 config.addSelection (self.containerName, self.selectionName, alg.selectionDecoration,
558 preselection=self.addSelectionToPreselection)
559
560 if self.chargeIDSelectionRun2 and config.geometry() >= LHCPeriod.Run3:
561 log.warning("ECIDS is only available for Run 2 and will not have any effect in Run 3.")
562
563 # Select electrons only if they don't appear to have flipped their charge.
564 if self.chargeIDSelectionRun2 and config.geometry() < LHCPeriod.Run3:
565 alg = config.createAlgorithm( 'CP::AsgSelectionAlg',
566 'ElectronChargeIDSelectionAlg' )
567 alg.selectionDecoration = 'chargeID' + selectionPostfix + ',as_char'
569 # Rerun the ECIDS BDT
570 config.addPrivateTool( 'selectionTool',
571 'AsgElectronChargeIDSelectorTool' )
572 alg.selectionTool.TrainingFile = \
573 'ElectronPhotonSelectorTools/ChargeID/ECIDS_20180731rel21Summer2018.root'
574 alg.selectionTool.WorkingPoint = 'Loose'
575 alg.selectionTool.CutOnBDT = -0.337671 # Loose 97%
576 else:
577 # Select from Derivation Framework flags
578 config.addPrivateTool( 'selectionTool', 'CP::AsgFlagSelectionTool' )
579 alg.selectionTool.selectionFlags = ["DFCommonElectronsECIDS"]
580
581 alg.particles = config.readName (self.containerName)
582 alg.preselection = config.getPreselection (self.containerName, self.selectionName)
583 config.addSelection (self.containerName, self.selectionName, alg.selectionDecoration,
584 preselection=self.addSelectionToPreselection)
585
586
588 """the ConfigBlock for the electron working point efficiency computation"""
589
590 def __init__(self) :
591 super (ElectronWorkingPointEfficiencyConfig, self).__init__ ()
592 self.setBlockName('ElectronWorkingPointEfficiency')
593 self.addDependency('ElectronWorkingPointSelection', required=True)
594 self.addDependency('EventSelection', required=False)
595 self.addDependency('EventSelectionMerger', required=False)
596 self.addOption ('containerName', '', type=str,
597 noneAction='error',
598 info="the name of the input container.")
599 self.addOption ('selectionName', '', type=str,
600 noneAction='error',
601 info="the name of the electron selection to define (e.g. `tight` or "
602 "`loose`).")
603 self.addOption ('postfix', None, type=str,
604 info="a postfix to apply to decorations and algorithm names. "
605 "Typically not needed here as `selectionName` is used internally.")
606 self.addOption ('identificationWP', None, type=str,
607 info="the ID WP to use. Supported ID WPs: `TightLH`, "
608 "`MediumLH`, `LooseBLayerLH`, `TightDNN`, `MediumDNN`, `LooseDNN`, "
609 "`TightNoCFDNN`, `MediumNoCFDNN`, `VeryLooseNoCF97DNN`, `NoID`.",
610 expertMode=["NoID"])
611 self.addOption ('isolationWP', None, type=str,
612 info="the isolation WP to use. Supported isolation WPs: "
613 "`HighPtCaloOnly`, `Loose_VarRad`, `Tight_VarRad`, `TightTrackOnly_"
614 "VarRad`, `TightTrackOnly_FixedRad`, `NonIso`.")
615 self.addOption ('noEffSF', False, type=bool,
616 info="disables the calculation of efficiencies and scale factors. "
617 "Experimental! only useful to test a new WP for which scale "
618 "factors are not available.",
619 expertMode=True)
620 self.addOption ('chargeIDSelectionRun2', False, type=bool,
621 info="whether to run the ECIDS tool. Only available for Run 2.")
622 self.addOption ('saveDetailedSF', True, type=bool,
623 info="save all the independent detailed object scale factors.")
624 self.addOption ('saveCombinedSF', False, type=bool,
625 info="save the combined object scale factor.")
626 self.addOption ('forceFullSimConfig', False, type=bool,
627 info="whether to force the tool to use the configuration meant for "
628 "full simulation samples. Only for testing purposes.")
629 self.addOption ('correlationModelId', 'SIMPLIFIED', type=str,
630 info="the correlation model to use for ID scale factors. "
631 "Supported models: `SIMPLIFIED`, `FULL`, `TOTAL`, `TOYS`.")
632 self.addOption ('correlationModelIso', 'SIMPLIFIED', type=str,
633 info="the correlation model to use for isolation scale factors, "
634 "Supported models: `SIMPLIFIED`, `FULL`, `TOTAL`, `TOYS`.")
635 self.addOption ('correlationModelReco', 'SIMPLIFIED', type=str,
636 info="the correlation model to use for reconstruction scale factors. "
637 "Supported models: `SIMPLIFIED`, `FULL`, `TOTAL`, `TOYS`.")
638 self.addOption('addChargeMisIDSF', False, type=bool,
639 info="adds scale factors for charge-misID.")
640
641 def instanceName (self) :
642 """Return the instance name for this block"""
643 if self.postfix is not None :
644 return self.containerName + '_' + self.selectionName + self.postfix
645 return self.containerName + '_' + self.selectionName
646
647 def makeAlgs (self, config) :
648
649 log = logging.getLogger('ElectronWorkingPointEfficiencyConfig')
650
652 log.warning("You are running ElectronWorkingPointSelectionConfig forcing full sim config")
653 log.warning("This is only intended to be used for testing purposes")
654
655 selectionPostfix = self.selectionName
656 if selectionPostfix != '' and selectionPostfix[0] != '_' :
657 selectionPostfix = '_' + selectionPostfix
658
659 # The setup below is inappropriate for Run 1
660 if config.geometry() is LHCPeriod.Run1:
661 raise ValueError ("Can't set up the ElectronWorkingPointSelectionConfig with %s, there must be something wrong!" % config.geometry().value)
662
663 postfix = self.postfix
664 if postfix is None :
665 postfix = self.selectionName
666 if postfix != '' and postfix[0] != '_' :
667 postfix = '_' + postfix
668
669 correlationModels = ["SIMPLIFIED", "FULL", "TOTAL", "TOYS"]
670 map_file = 'ElectronEfficiencyCorrection/2015_2025/rel22.2/2025_Run2Rel22_Recommendation_v3/map1.txt' \
671 if config.geometry() is LHCPeriod.Run2 else \
672 'ElectronEfficiencyCorrection/2015_2025/rel22.2/2025_Run3_Consolidated_Recommendation_v4/map2.txt'
673 sfList = []
674 # Set up the RECO electron efficiency correction algorithm:
675 if config.dataType() is not DataType.Data and not self.noEffSF:
676 if 'DNN' in self.identificationWP:
677 raise ValueError('DNN does not yet have efficiency correction, '
678 'please disable it by setting `noEffSF` to True.')
679
680 alg = config.createAlgorithm( 'CP::ElectronEfficiencyCorrectionAlg',
681 'ElectronEfficiencyCorrectionAlgReco' )
682 config.addPrivateTool( 'efficiencyCorrectionTool',
683 'AsgElectronEfficiencyCorrectionTool' )
684 alg.scaleFactorDecoration = 'el_reco_effSF' + selectionPostfix + '_%SYS%'
685 alg.efficiencyCorrectionTool.MapFilePath = map_file
686 alg.efficiencyCorrectionTool.RecoKey = "Reconstruction"
687 if self.correlationModelReco not in correlationModels:
688 raise ValueError('Invalid correlation model for reconstruction efficiency, '
689 f'has to be one of: {", ".join(correlationModels)}')
690 if config.geometry() >= LHCPeriod.Run3 and self.correlationModelReco != "TOTAL":
691 log.warning("Only TOTAL correlation model is currently supported "
692 "for reconstruction efficiency correction in Run 3.")
693 alg.efficiencyCorrectionTool.CorrelationModel = "TOTAL"
694 else:
695 alg.efficiencyCorrectionTool.CorrelationModel = self.correlationModelReco
696 if config.dataType() is DataType.FastSim:
697 alg.efficiencyCorrectionTool.ForceDataType = (
698 PATCore.ParticleDataType.Full if self.forceFullSimConfig
699 else PATCore.ParticleDataType.Fast)
700 elif config.dataType() is DataType.FullSim:
701 alg.efficiencyCorrectionTool.ForceDataType = \
702 PATCore.ParticleDataType.Full
703 alg.outOfValidity = 2 #silent
704 alg.outOfValidityDeco = 'el_reco_bad_eff' + selectionPostfix
705 alg.electrons = config.readName (self.containerName)
706 alg.preselection = config.getPreselection (self.containerName, self.selectionName)
708 config.addOutputVar (self.containerName, alg.scaleFactorDecoration,
709 'reco_effSF' + postfix)
710 sfList += [alg.scaleFactorDecoration]
711
712 # Set up the ID electron efficiency correction algorithm:
713 if config.dataType() is not DataType.Data and not self.noEffSF and self.identificationWP != 'NoID':
714
715 alg = config.createAlgorithm( 'CP::ElectronEfficiencyCorrectionAlg',
716 'ElectronEfficiencyCorrectionAlgID' )
717 config.addPrivateTool( 'efficiencyCorrectionTool',
718 'AsgElectronEfficiencyCorrectionTool' )
719 alg.scaleFactorDecoration = 'el_id_effSF' + selectionPostfix + '_%SYS%'
720 alg.efficiencyCorrectionTool.MapFilePath = map_file
721 alg.efficiencyCorrectionTool.IdKey = self.identificationWP.replace("LH","")
722 if self.correlationModelId not in correlationModels:
723 raise ValueError('Invalid correlation model for identification efficiency, '
724 f'has to be one of: {", ".join(correlationModels)}')
725 alg.efficiencyCorrectionTool.CorrelationModel = self.correlationModelId
726 if config.dataType() is DataType.FastSim:
727 alg.efficiencyCorrectionTool.ForceDataType = (
728 PATCore.ParticleDataType.Full if self.forceFullSimConfig
729 else PATCore.ParticleDataType.Fast)
730 elif config.dataType() is DataType.FullSim:
731 alg.efficiencyCorrectionTool.ForceDataType = \
732 PATCore.ParticleDataType.Full
733 alg.outOfValidity = 2 #silent
734 alg.outOfValidityDeco = 'el_id_bad_eff' + selectionPostfix
735 alg.electrons = config.readName (self.containerName)
736 alg.preselection = config.getPreselection (self.containerName, self.selectionName)
737 if self.saveDetailedSF:
738 config.addOutputVar (self.containerName, alg.scaleFactorDecoration,
739 'id_effSF' + postfix)
740 sfList += [alg.scaleFactorDecoration]
741
742 # Set up the ISO electron efficiency correction algorithm:
743 if config.dataType() is not DataType.Data and self.isolationWP != 'NonIso' and not self.noEffSF:
744 alg = config.createAlgorithm( 'CP::ElectronEfficiencyCorrectionAlg',
745 'ElectronEfficiencyCorrectionAlgIsol' )
746 config.addPrivateTool( 'efficiencyCorrectionTool',
747 'AsgElectronEfficiencyCorrectionTool' )
748 alg.scaleFactorDecoration = 'el_isol_effSF' + selectionPostfix + '_%SYS%'
749 alg.efficiencyCorrectionTool.MapFilePath = map_file
750 alg.efficiencyCorrectionTool.IdKey = self.identificationWP.replace("LH","")
751 alg.efficiencyCorrectionTool.IsoKey = self.isolationWP
752 if self.correlationModelIso not in correlationModels:
753 raise ValueError('Invalid correlation model for isolation efficiency, '
754 f'has to be one of: {", ".join(correlationModels)}')
755 if self.correlationModelIso != 'TOTAL':
756 log.warning("Only TOTAL correlation model is currently supported "
757 "for isolation efficiency correction in Run 3.")
758 alg.efficiencyCorrectionTool.CorrelationModel = "TOTAL"
759 if config.dataType() is DataType.FastSim:
760 alg.efficiencyCorrectionTool.ForceDataType = (
761 PATCore.ParticleDataType.Full if self.forceFullSimConfig
762 else PATCore.ParticleDataType.Fast)
763 elif config.dataType() is DataType.FullSim:
764 alg.efficiencyCorrectionTool.ForceDataType = \
765 PATCore.ParticleDataType.Full
766 alg.outOfValidity = 2 #silent
767 alg.outOfValidityDeco = 'el_isol_bad_eff' + selectionPostfix
768 alg.electrons = config.readName (self.containerName)
769 alg.preselection = config.getPreselection (self.containerName, self.selectionName)
770 if self.saveDetailedSF:
771 config.addOutputVar (self.containerName, alg.scaleFactorDecoration,
772 'isol_effSF' + postfix)
773 sfList += [alg.scaleFactorDecoration]
774
775 if (self.chargeIDSelectionRun2 and config.geometry() < LHCPeriod.Run3 and
776 config.dataType() is not DataType.Data and not self.noEffSF):
777 alg = config.createAlgorithm( 'CP::ElectronEfficiencyCorrectionAlg',
778 'ElectronEfficiencyCorrectionAlgEcids' )
779 config.addPrivateTool( 'efficiencyCorrectionTool',
780 'AsgElectronEfficiencyCorrectionTool' )
781 alg.scaleFactorDecoration = 'el_ecids_effSF' + selectionPostfix + '_%SYS%'
782 if self.isolationWP != 'Tight_VarRad':
783 raise ValueError('ECIDS SFs are supported only for Tight_VarRad isolation.')
784 if self.identificationWP == 'LooseBLayerLH':
785 ecids_lh = 'loose'
786 elif self.identificationWP == 'MediumLH':
787 ecids_lh = 'medium'
788 elif self.identificationWP == 'TightLH':
789 ecids_lh = 'tight'
790 else:
791 raise ValueError('ECIDS SFs are supported only for ID LooseBLayerLH, MediumLH, or TightLH')
792
793 alg.efficiencyCorrectionTool.CorrelationModel = "TOTAL"
794 alg.efficiencyCorrectionTool.CorrectionFileNameList = \
795 [f'ElectronEfficiencyCorrection/2015_2025/rel22.2/2025_Run2Rel22_Recommendation_v2/ecids/efficiencySF.ChargeID.{ecids_lh}_ECIDS_Tight_VarRad.root']
796 if config.dataType() is DataType.FastSim:
797 alg.efficiencyCorrectionTool.ForceDataType = (
798 PATCore.ParticleDataType.Full if self.forceFullSimConfig
799 else PATCore.ParticleDataType.Fast)
800 elif config.dataType() is DataType.FullSim:
801 alg.efficiencyCorrectionTool.ForceDataType = \
802 PATCore.ParticleDataType.Full
803 alg.outOfValidity = 2 #silent
804 alg.outOfValidityDeco = 'el_ecids_bad_eff' + selectionPostfix
805 alg.electrons = config.readName (self.containerName)
806 alg.preselection = config.getPreselection (self.containerName, self.selectionName)
807 if self.saveDetailedSF:
808 config.addOutputVar (self.containerName, alg.scaleFactorDecoration,
809 'ecids_effSF' + postfix)
810 sfList += [alg.scaleFactorDecoration]
811
812 if self.addChargeMisIDSF and config.dataType() is not DataType.Data and not self.noEffSF and config.geometry() >= LHCPeriod.Run3:
813 log.warning("Charge mis-ID SFs are only available for Run 2 and will not have any effect in Run 3.")
814
815 elif self.addChargeMisIDSF and config.dataType() is not DataType.Data and not self.noEffSF and config.geometry() < LHCPeriod.Run3:
816 alg = config.createAlgorithm( 'CP::ElectronEfficiencyCorrectionAlg',
817 'ElectronEfficiencyCorrectionAlgMisid' )
818 config.addPrivateTool( 'efficiencyCorrectionTool',
819 'CP::ElectronChargeEfficiencyCorrectionTool' )
820 alg.scaleFactorDecoration = 'el_charge_misid_effSF' + selectionPostfix + '_%SYS%'
821 if self.isolationWP != 'Tight_VarRad':
822 raise ValueError('Charge mis-ID SFs are supported only for Tight_VarRad isolation.')
823 if self.identificationWP == 'LooseBLayerLH':
824 misid_lh = 'LooseAndBLayerLLH'
825 elif self.identificationWP == 'MediumLH':
826 misid_lh = 'MediumLLH'
827 elif self.identificationWP == 'TightLH':
828 misid_lh = 'TightLLH'
829 else:
830 raise ValueError('Charge mis-ID SFs are supported only for ID LooseBLayerLH, MediumLH, or TightLH')
831 misid_suffix = '_ECIDSloose' if self.chargeIDSelectionRun2 else ''
832
833 alg.efficiencyCorrectionTool.CorrectionFileName = \
834 f'ElectronEfficiencyCorrection/2015_2025/rel22.2/2025_Run2Rel22_Recommendation_v2/charge_misID/chargeEfficiencySF.{misid_lh}_d0z0_TightVarRad{misid_suffix}.root'
835 if config.dataType() is DataType.FastSim:
836 alg.efficiencyCorrectionTool.ForceDataType = (
837 PATCore.ParticleDataType.Full if self.forceFullSimConfig
838 else PATCore.ParticleDataType.Fast)
839 elif config.dataType() is DataType.FullSim:
840 alg.efficiencyCorrectionTool.ForceDataType = \
841 PATCore.ParticleDataType.Full
842 alg.outOfValidity = 2 #silent
843 alg.outOfValidityDeco = 'el_misid_bad_eff' + selectionPostfix
844 alg.electrons = config.readName (self.containerName)
845 alg.preselection = config.getPreselection (self.containerName, self.selectionName)
846 if self.saveDetailedSF:
847 config.addOutputVar (self.containerName, alg.scaleFactorDecoration,
848 'charge_misid_effSF' + postfix)
849 sfList += [alg.scaleFactorDecoration]
850
851 if config.dataType() is not DataType.Data and not self.noEffSF and self.saveCombinedSF:
852 alg = config.createAlgorithm( 'CP::AsgObjectScaleFactorAlg',
853 'ElectronCombinedEfficiencyScaleFactorAlg' )
854 alg.particles = config.readName (self.containerName)
855 alg.inScaleFactors = sfList
856 alg.outScaleFactor = 'effSF' + postfix + '_%SYS%'
857 config.addOutputVar (self.containerName, alg.outScaleFactor, 'effSF' + postfix)
858
860
861 def __init__ (self) :
862 super (ElectronTriggerAnalysisSFBlock, self).__init__ ()
863 self.addDependency('EventSelection', required=False)
864 self.addDependency('EventSelectionMerger', required=False)
865 self.addOption ('triggerChainsPerYear', {}, type=dict,
866 info="a dictionary with key (string) the year and value (list of "
867 "strings) the trigger chains.")
868 self.addOption ('electronID', '', type=str,
869 info="the electron ID WP to use.")
870 self.addOption ('electronIsol', '', type=str,
871 info="the electron isolation WP to use.")
872 self.addOption ('saveEff', False, type=bool,
873 info="define whether we decorate also the trigger scale efficiency.")
874 self.addOption ('prefixSF', 'trigEffSF', type=str,
875 info="the decoration prefix for trigger scale factors.")
876 self.addOption ('prefixEff', 'trigEff', type=str,
877 info="the decoration prefix for MC trigger efficiencies.")
878 self.addOption ('includeAllYearsPerRun', False, type=bool,
879 info="all configured years in the LHC run will "
880 "be included in all jobs.")
881 self.addOption ('removeHLTPrefix', True, type=bool,
882 info="remove the HLT prefix from trigger chain names.")
883 self.addOption ('useToolKeyAsOutput', False, type=bool,
884 info="use the tool trigger key as output.")
885 self.addOption ('containerName', '', type=str,
886 info="the input electron container, with a possible selection, in "
887 "the format `container` or `container.selection`.")
888
889 def instanceName (self) :
890 """Return the instance name for this block"""
891 return self.containerName
892
893 def makeAlgs (self, config) :
894
895 if config.dataType() is not DataType.Data:
896 log = logging.getLogger('ElectronTriggerSFConfig')
897
899 log.warning('`includeAllYearsPerRun` is set to True, but `useToolKeyAsOutput` is set to False. '
900 'This will cause multiple branches to be written out with the same content.')
901
902 # Dictionary from TrigGlobalEfficiencyCorrection/Triggers.cfg
903 # Key is trigger chain (w/o HLT prefix)
904 # Value is empty for single leg trigger or list of legs
905 triggerDict = TriggerDict()
906
907 # currently recommended versions
908 version_Run2 = "2015_2025/rel22.2/2025_Run2Rel22_Recommendation_v3"
909 map_Run2 = f"ElectronEfficiencyCorrection/{version_Run2}/map1.txt"
910 version_Run3 = "2015_2025/rel22.2/2025_Run3_Consolidated_Recommendation_v4"
911 map_Run3 = f"ElectronEfficiencyCorrection/{version_Run3}/map2.txt"
912
913 version = version_Run2 if config.geometry() is LHCPeriod.Run2 else version_Run3
914 # Dictionary from TrigGlobalEfficiencyCorrection/MapKeys.cfg
915 # Key is year_leg
916 # Value is list of configs available, first one will be used
917 mapKeysDict = MapKeysDict(version)
918
919 # helper function for leg filtering, very hardcoded but allows autoconfiguration
920 def filterConfFromMap(conf, electronMapKeys):
921 if not conf:
922 raise ValueError("No configuration found for trigger chain.")
923 if len(conf) == 1:
924 return conf[0]
925
926 for c in conf:
927 if c in electronMapKeys:
928 return c
929
930 return conf[0]
931
933 years = [int(year) for year in self.triggerChainsPerYear.keys()]
934 else:
935 from TriggerAnalysisAlgorithms.TriggerAnalysisSFConfig import (
936 get_input_years)
937 years = get_input_years(config)
938
939 # prepare keys
940 import ROOT
941 triggerChainsPerYear_Run2 = {}
942 triggerChainsPerYear_Run3 = {}
943 for year, chains in self.triggerChainsPerYear.items():
944 if not chains:
945 log.warning("No trigger chains configured for year %s. "
946 "Assuming this is intended, no Electron trigger SF will be computed.", year)
947 continue
948
949 chains_split = [chain.replace("HLT_", "").replace(" || ", "_OR_") for chain in chains]
950 if int(year) >= 2022:
951 triggerChainsPerYear_Run3[str(year)] = ' || '.join(chains_split)
952 else:
953 triggerChainsPerYear_Run2[str(year)] = ' || '.join(chains_split)
954 electronMapKeys_Run2 = ROOT.std.map("string", "string")()
955 electronMapKeys_Run3 = ROOT.std.map("string", "string")()
956
957 sc_Run2 = ROOT.TrigGlobalEfficiencyCorrectionTool.suggestElectronMapKeys(triggerChainsPerYear_Run2, version_Run2, electronMapKeys_Run2)
958 sc_Run3 = ROOT.TrigGlobalEfficiencyCorrectionTool.suggestElectronMapKeys(triggerChainsPerYear_Run3, version_Run3, electronMapKeys_Run3)
959 if sc_Run2.code() != 2 or sc_Run3.code() != 2:
960 raise RuntimeError("Failed to suggest electron map keys")
961 electronMapKeys = dict(electronMapKeys_Run2) | dict(electronMapKeys_Run3)
962
963 # collect configurations
964 from TriggerAnalysisAlgorithms.TriggerAnalysisConfig import is_year_in_current_period
965 triggerConfigs = {}
966 for year in years:
967 if not is_year_in_current_period(config, year):
968 continue
969
970 triggerChains = self.triggerChainsPerYear.get(int(year), self.triggerChainsPerYear.get(str(year), []))
971 for chain in triggerChains:
972 chain = chain.replace(" || ", "_OR_")
973 chain_noHLT = chain.replace("HLT_", "")
974 chain_out = chain_noHLT if self.removeHLTPrefix else chain
975 legs = triggerDict[chain_noHLT]
976 if not legs:
977 if chain_noHLT[0] == 'e' and chain_noHLT[1].isdigit:
978 chain_key = f"{year}_{chain_noHLT}"
979 chain_conf = mapKeysDict[chain_key][0]
980 triggerConfigs[chain_conf if self.useToolKeyAsOutput else chain_out] = chain_conf
981 else:
982 for leg in legs:
983 if leg[0] == 'e' and leg[1].isdigit:
984 leg_out = leg if self.removeHLTPrefix else f"HLT_{leg}"
985 leg_key = f"{year}_{leg}"
986 leg_conf = filterConfFromMap(mapKeysDict[leg_key], electronMapKeys)
987 triggerConfigs[leg_conf if self.useToolKeyAsOutput else leg_out] = leg_conf
988
989 decorations = [self.prefixSF]
990 if self.saveEff:
991 decorations += [self.prefixEff]
992
993 for label, conf in triggerConfigs.items():
994 for deco in decorations:
995 alg = config.createAlgorithm('CP::ElectronEfficiencyCorrectionAlg',
996 'EleTrigEfficiencyCorrectionsAlg' + deco +
997 '_' + label)
998 config.addPrivateTool( 'efficiencyCorrectionTool',
999 'AsgElectronEfficiencyCorrectionTool' )
1000
1001 # Reproduce config from TrigGlobalEfficiencyAlg
1002 alg.efficiencyCorrectionTool.MapFilePath = map_Run3 if config.geometry() is LHCPeriod.Run3 else map_Run2
1003 alg.efficiencyCorrectionTool.IdKey = self.electronID.replace("LH","")
1004 alg.efficiencyCorrectionTool.IsoKey = self.electronIsol
1005 alg.efficiencyCorrectionTool.TriggerKey = (
1006 ("Eff_" if deco == self.prefixEff else "") + conf)
1007 alg.efficiencyCorrectionTool.CorrelationModel = "TOTAL"
1008 alg.efficiencyCorrectionTool.ForceDataType = \
1009 PATCore.ParticleDataType.Full
1010
1011 alg.scaleFactorDecoration = f"el_{deco}_{label}_%SYS%"
1012
1013 alg.outOfValidity = 2 #silent
1014 alg.outOfValidityDeco = f"bad_eff_ele{deco}_{label}"
1015 alg.electrons = config.readName (self.containerName)
1016 alg.preselection = config.getPreselection (self.containerName, "")
1017 config.addOutputVar (self.containerName, alg.scaleFactorDecoration, f"{deco}_{label}")
1018
1019
1020class ElectronLRTMergedConfig (ConfigBlock) :
1021 def __init__ (self) :
1022 super (ElectronLRTMergedConfig, self).__init__ ()
1023 self.addOption (
1024 'inputElectrons', 'Electrons', type=str,
1025 noneAction='error',
1026 info="the name of the input electron container."
1027 )
1028 self.addOption (
1029 'inputLRTElectrons', 'LRTElectrons', type=str,
1030 noneAction='error',
1031 info="the name of the input LRT electron container."
1032 )
1033 self.addOption (
1034 'containerName', 'Electrons_LRTMerged', type=str,
1035 noneAction='error',
1036 info="the name of the output container after LRT merging."
1037 )
1038
1039 def instanceName (self) :
1040 """Return the instance name for this block"""
1041 return self.containerName
1042
1043 def makeAlgs (self, config) :
1044
1045 if config.isPhyslite() :
1046 raise(RuntimeError("Electron LRT merging is not available in Physlite mode"))
1047
1048 alg = config.createAlgorithm( "CP::ElectronLRTMergingAlg", "ElectronLRTMergingAlg" )
1049 alg.PromptElectronLocation = self.inputElectrons
1050 alg.LRTElectronLocation = self.inputLRTElectrons
1051 alg.OutputCollectionName = self.containerName
1052 alg.CreateViewCollection = False
1053
1054
1055@groupBlocks
std::string replace(std::string s, const std::string &s2, const std::string &s3)
Definition hcg.cxx:310
T * get(TKey *tobj)
get a TObject* from a TKey* (why can't a TObject be a TKey?)
Definition hcg.cxx:130
std::vector< std::string > split(const std::string &s, const std::string &t=":")
Definition hcg.cxx:177