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