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