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