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