-
Adding d0 and z0sintheta Significance for EasyJet Ntuple Production
Adding d0 and z0sintheta Significance for EasyJet Ntuple Production
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
ElectronAnalysisConfig.py 46.81 KiB
# Copyright (C) 2002-2025 CERN for the benefit of the ATLAS collaboration
# AnaAlgorithm import(s):
from AnalysisAlgorithmsConfig.ConfigBlock import ConfigBlock
from AthenaCommon.SystemOfUnits import GeV
from AthenaConfiguration.Enums import LHCPeriod
from AnalysisAlgorithmsConfig.ConfigAccumulator import DataType
from TrigGlobalEfficiencyCorrection.TriggerLeg_DictHelpers import TriggerDict, MapKeysDict
from Campaigns.Utils import Campaign
from AthenaCommon.Logging import logging
# E/gamma import(s).
from xAODEgamma.xAODEgammaParameters import xAOD
import PATCore.ParticleDataType
class ElectronCalibrationConfig (ConfigBlock) :
"""the ConfigBlock for the electron four-momentum correction"""
def __init__ (self, containerName='') :
super (ElectronCalibrationConfig, self).__init__ ()
self.setBlockName('Electrons')
self.addOption ('inputContainer', '', type=str,
info="select electron input container, by default set to Electrons")
self.addOption ('containerName', containerName, type=str,
noneAction='error',
info="the name of the output container after calibration.")
self.addOption ('ESModel', '', type=str,
info="flag of egamma calibration recommendation.")
self.addOption ('decorrelationModel', '1NP_v1', type=str,
info="egamma energy scale decorrelationModel. The default is 1NP_v1. "
"Supported Model: 1NP_v1, FULL_v1.")
self.addOption ('postfix', '', type=str,
info="a postfix to apply to decorations and algorithm names. Typically "
"not needed here since the calibration is common to all electrons.")
self.addOption ('crackVeto', False, type=bool,
info="whether to perform LAr crack veto based on the cluster eta, "
"i.e. remove electrons within 1.37<|eta|<1.52. The default "
"is False.")
self.addOption ('isolationCorrection', False, type=bool,
info="whether or not to perform isolation corrections (leakage "
"corrections), i.e. set up an instance of "
"CP::EgammaIsolationCorrectionAlg.")
self.addOption ('recalibratePhyslite', True, type=bool,
info="whether to run the CP::EgammaCalibrationAndSmearingAlg on "
"PHYSLITE derivations. The default is True.")
self.addOption ('minPt', 4.5*GeV, type=float,
info="the minimum pT cut to apply to calibrated electrons. "
"The default is 4.5 GeV.")
self.addOption ('maxEta', 2.47, type=float,
info="maximum electron |eta| (float). The default is 2.47.")
self.addOption ('forceFullSimConfig', False, type=bool,
info="whether to force the tool to use the configuration meant for "
"full simulation samples. Only for testing purposes. The default "
"is False.")
self.addOption ('splitCalibrationAndSmearing', False, type=bool,
info="EXPERIMENTAL: This splits the EgammaCalibrationAndSmearingTool "
" into two steps. The first step applies a baseline calibration that "
"is not affected by systematics. The second step then applies the "
"systematics dependent corrections. The net effect is that the "
"slower first step only has to be run once, while the second is run "
"once per systematic. ATLASG-2358")
self.addOption ('decorateTruth', False, type=bool,
info="decorate truth particle information on the reconstructed one")
self.addOption ('decorateCaloClusterEta', False, type=bool,
info="decorate the calo cluster eta on the reconstructed one")
self.addOption ('writeTrackD0Z0', False, type = bool,
info="save the d0 significance and z0sinTheta variables so they can be written out")
self.addOption ('decorateEmva', False, type=bool,
info="decorate E_mva_only on the objects (needed for columnar tools/PHYSLITE)")
self.addOption ('decorateSamplingPattern', False, type=bool,
info="add samplingPattern decorations to clusters as part of PHYSLITE")
def makeCalibrationAndSmearingAlg (self, config, name) :
"""Create the calibration and smearing algorithm
Factoring this out into its own function, as we want to
instantiate it in multiple places"""
log = logging.getLogger('ElectronCalibrationConfig')
# Set up the calibration and smearing algorithm:
alg = config.createAlgorithm( 'CP::EgammaCalibrationAndSmearingAlg', name + self.postfix )
config.addPrivateTool( 'calibrationAndSmearingTool',
'CP::EgammaCalibrationAndSmearingTool' )
# Set default ESModel per period
if self.ESModel:
alg.calibrationAndSmearingTool.ESModel = self.ESModel
else:
if config.geometry() is LHCPeriod.Run2:
alg.calibrationAndSmearingTool.ESModel = 'es2023_R22_Run2_v1'
elif config.geometry() is LHCPeriod.Run3:
alg.calibrationAndSmearingTool.ESModel = 'es2022_R22_PRE'
elif config.geometry() is LHCPeriod.Run4:
log.warning("No ESModel set for Run4, using Run 3 model instead")
alg.calibrationAndSmearingTool.ESModel = 'es2022_R22_PRE'
else:
raise ValueError (f"Can't set up the ElectronCalibrationConfig with {config.geometry().value}, "
"there must be something wrong!")
alg.calibrationAndSmearingTool.decorrelationModel = self.decorrelationModel
alg.calibrationAndSmearingTool.useFastSim = (
0 if self.forceFullSimConfig
else int( config.dataType() is DataType.FastSim ))
alg.calibrationAndSmearingTool.decorateEmva = self.decorateEmva
alg.egammas = config.readName (self.containerName)
alg.egammasOut = config.copyName (self.containerName)
alg.preselection = config.getPreselection (self.containerName, '')
return alg
def makeAlgs (self, config) :
log = logging.getLogger('ElectronCalibrationConfig')
if self.forceFullSimConfig:
log.warning("You are running ElectronCalibrationConfig forcing full sim config")
log.warning(" This is only intended to be used for testing purposes")
inputContainer = "AnalysisElectrons" if config.isPhyslite() else "Electrons"
if self.inputContainer:
inputContainer = self.inputContainer
config.setSourceName (self.containerName, inputContainer)
# Decorate calo cluster eta if required
if self.decorateCaloClusterEta:
alg = config.createAlgorithm( 'CP::EgammaCaloClusterEtaAlg',
'ElectronEgammaCaloClusterEtaAlg' + self.postfix,
reentrant=True )
alg.particles = config.readName(self.containerName)
config.addOutputVar (self.containerName, 'caloEta2', 'caloEta2', noSys=True)
if self.decorateSamplingPattern:
config.createAlgorithm( 'CP::EgammaSamplingPatternDecoratorAlg', 'EgammaSamplingPatternDecoratorAlg' + self.postfix )
# Set up a shallow copy to decorate
if config.wantCopy (self.containerName) :
alg = config.createAlgorithm( 'CP::AsgShallowCopyAlg', 'ElectronShallowCopyAlg' + self.postfix )
alg.input = config.readName (self.containerName)
alg.output = config.copyName (self.containerName)
# Set up the eta-cut on all electrons prior to everything else
alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronEtaCutAlg' + self.postfix )
alg.selectionDecoration = 'selectEta' + self.postfix + ',as_bits'
config.addPrivateTool( 'selectionTool', 'CP::AsgPtEtaSelectionTool' )
alg.selectionTool.maxEta = self.maxEta
if self.crackVeto:
alg.selectionTool.etaGapLow = 1.37
alg.selectionTool.etaGapHigh = 1.52
alg.selectionTool.useClusterEta = True
alg.particles = config.readName (self.containerName)
alg.preselection = config.getPreselection (self.containerName, '')
config.addSelection (self.containerName, '', alg.selectionDecoration)
# Select electrons only with good object quality.
alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronObjectQualityAlg' + self.postfix )
alg.selectionDecoration = 'goodOQ' + self.postfix + ',as_bits'
config.addPrivateTool( 'selectionTool', 'CP::EgammaIsGoodOQSelectionTool' )
alg.selectionTool.Mask = xAOD.EgammaParameters.BADCLUSELECTRON
alg.particles = config.readName (self.containerName)
alg.preselection = config.getPreselection (self.containerName, '')
config.addSelection (self.containerName, '', alg.selectionDecoration)
if not self.splitCalibrationAndSmearing :
# Set up the calibration and smearing algorithm:
alg = self.makeCalibrationAndSmearingAlg (config, 'ElectronCalibrationAndSmearingAlg')
if config.isPhyslite() and not self.recalibratePhyslite :
alg.skipNominal = True
else:
# This splits the EgammaCalibrationAndSmearingTool into two
# steps. The first step applies a baseline calibration that
# is not affected by systematics. The second step then
# applies the systematics dependent corrections. The net
# effect is that the slower first step only has to be run
# once, while the second is run once per systematic.
#
# For now (22 May 24) this has to happen in the same job, as
# the output of the first step is not part of PHYSLITE, and
# even for the nominal the output of the first and second
# step are different. In the future the plan is to put both
# the output of the first and second step into PHYSLITE,
# allowing to skip the first step when running on PHYSLITE.
#
# WARNING: All of this is experimental, see: ATLASG-2358
# Set up the calibration algorithm:
alg = self.makeCalibrationAndSmearingAlg (config, 'ElectronBaseCalibrationAlg')
# turn off systematics for the calibration step
alg.noToolSystematics = True
# turn off smearing for the calibration step
alg.calibrationAndSmearingTool.doSmearing = False
# Set up the smearing algorithm:
alg = self.makeCalibrationAndSmearingAlg (config, 'ElectronCalibrationSystematicsAlg')
# turn off scale corrections for the smearing step
alg.calibrationAndSmearingTool.doScaleCorrection = False
alg.calibrationAndSmearingTool.useMVACalibration = False
alg.calibrationAndSmearingTool.decorateEmva = False
if self.minPt > 0 :
# Set up the the pt selection
alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronPtCutAlg' + self.postfix )
alg.selectionDecoration = 'selectPt' + self.postfix + ',as_bits'
config.addPrivateTool( 'selectionTool', 'CP::AsgPtEtaSelectionTool' )
alg.selectionTool.minPt = self.minPt
alg.particles = config.readName (self.containerName)
alg.preselection = config.getPreselection (self.containerName, '')
config.addSelection (self.containerName, '', alg.selectionDecoration,
preselection=True)
# Set up the isolation correction algorithm:
if self.isolationCorrection:
alg = config.createAlgorithm( 'CP::EgammaIsolationCorrectionAlg',
'ElectronIsolationCorrectionAlg' + self.postfix )
config.addPrivateTool( 'isolationCorrectionTool',
'CP::IsolationCorrectionTool' )
alg.isolationCorrectionTool.IsMC = config.dataType() is not DataType.Data
alg.isolationCorrectionTool.AFII_corr = (
0 if self.forceFullSimConfig
else config.dataType() is DataType.FastSim)
alg.egammas = config.readName (self.containerName)
alg.egammasOut = config.copyName (self.containerName)
alg.preselection = config.getPreselection (self.containerName, '')
# Additional decorations
if self.writeTrackD0Z0:
alg = config.createAlgorithm( 'CP::AsgLeptonTrackDecorationAlg',
'LeptonTrackDecorator' + self.containerName + self.postfix,
reentrant=True )
alg.particles = config.readName (self.containerName)
alg = config.createAlgorithm( 'CP::AsgEnergyDecoratorAlg', 'EnergyDecorator' + self.containerName + self.postfix )
alg.particles = config.readName(self.containerName)
config.addOutputVar (self.containerName, 'pt', 'pt')
config.addOutputVar (self.containerName, 'eta', 'eta', noSys=True)
config.addOutputVar (self.containerName, 'phi', 'phi', noSys=True)
config.addOutputVar (self.containerName, 'e_%SYS%', 'e')
config.addOutputVar (self.containerName, 'charge', 'charge', noSys=True)
if self.writeTrackD0Z0:
config.addOutputVar (self.containerName, 'd0_%SYS%', 'd0', noSys=True)
config.addOutputVar (self.containerName, 'd0sig_%SYS%', 'd0sig', noSys=True)
config.addOutputVar (self.containerName, 'z0sintheta_%SYS%', 'z0sintheta', noSys=True)
config.addOutputVar (self.containerName, 'z0sinthetasig_%SYS%', 'z0sinthetasig', noSys=True)
# decorate truth information on the reconstructed object:
if self.decorateTruth and config.dataType() is not DataType.Data:
config.addOutputVar (self.containerName, "truthType", "truth_type", noSys=True)
config.addOutputVar (self.containerName, "truthOrigin", "truth_origin", noSys=True)
config.addOutputVar (self.containerName, "firstEgMotherPdgId", "truth_firstEgMotherPdgId", noSys=True)
config.addOutputVar (self.containerName, "firstEgMotherTruthOrigin", "truth_firstEgMotherTruthOrigin", noSys=True)
config.addOutputVar (self.containerName, "firstEgMotherTruthType", "truth_firstEgMotherTruthType", noSys=True)
class ElectronWorkingPointConfig (ConfigBlock) :
"""the ConfigBlock for the electron working point
This may at some point be split into multiple blocks (29 Aug 22)."""
def __init__ (self, containerName='', selectionName='') :
super (ElectronWorkingPointConfig, self).__init__ ()
self.addOption ('containerName', containerName, type=str,
noneAction='error',
info="the name of the input container.")
self.addOption ('selectionName', selectionName, type=str,
noneAction='error',
info="the name of the electron selection to define (e.g. tight or "
"loose).")
self.addOption ('postfix', None, type=str,
info="a postfix to apply to decorations and algorithm names. "
"Typically not needed here as selectionName is used internally.")
self.addOption ('trackSelection', True, type=bool,
info="whether or not to set up an instance of "
"CP::AsgLeptonTrackSelectionAlg, with the recommended d_0 and "
"z_0 sin(theta) cuts. The default is True.")
self.addOption ('maxD0Significance', 5, type=float,
info="maximum d0 significance used for the trackSelection"
"The default is 5")
self.addOption ('maxDeltaZ0SinTheta', 0.5, type=float,
info="maximum z0sinTheta in mm used for the trackSelection"
"The default is 0.5 mm")
self.addOption ('identificationWP', None, type=str,
info="the ID WP (string) to use. Supported ID WPs: TightLH, "
"MediumLH, LooseBLayerLH, TightDNN, MediumDNN, LooseDNN, "
"TightNoCFDNN, MediumNoCFDNN, VeryLooseNoCF97DNN.")
self.addOption ('isolationWP', None, type=str,
info="the isolation WP (string) to use. Supported isolation WPs: "
"HighPtCaloOnly, Loose_VarRad, Tight_VarRad, TightTrackOnly_"
"VarRad, TightTrackOnly_FixedRad, NonIso.")
self.addOption ('addSelectionToPreselection', True, type=bool,
info="whether to retain only electrons satisfying the working point "
"requirements. The default is True.")
self.addOption ('closeByCorrection', False, type=bool,
info="whether to use close-by-corrected isolation working points")
self.addOption ('recomputeID', False, type=bool,
info="whether to rerun the ID LH/DNN. The default is False, i.e. to use "
"derivation flags.")
self.addOption ('chargeIDSelectionRun2', False, type=bool,
info="whether to run the ECIDS tool. Only available for run 2. "
"The default is False.")
self.addOption ('recomputeChargeID', False, type=bool,
info="whether to rerun the ECIDS. The default is False, i.e. to use "
"derivation flags.")
self.addOption ('doFSRSelection', False, type=bool,
info="whether to accept additional electrons close to muons for "
"the purpose of FSR corrections to these muons. Expert feature "
"requested by the H4l analysis running on PHYSLITE. "
"The default is False.")
self.addOption ('noEffSF', False, type=bool,
info="disables the calculation of efficiencies and scale factors. "
"Experimental! only useful to test a new WP for which scale "
"factors are not available. The default is False.")
self.addOption ('saveDetailedSF', True, type=bool,
info="save all the independent detailed object scale factors. "
"The default is True.")
self.addOption ('saveCombinedSF', False, type=bool,
info="save the combined object scale factor. "
"The default is False.")
self.addOption ('forceFullSimConfig', False, type=bool,
info="whether to force the tool to use the configuration meant for "
"full simulation samples. Only for testing purposes. "
"The default is False.")
self.addOption ('correlationModelId', 'SIMPLIFIED', type=str,
info="the correlation model (string) to use for ID scale factors "
"Supported models: SIMPLIFIED (default), FULL, TOTAL, TOYS")
self.addOption ('correlationModelIso', 'SIMPLIFIED', type=str,
info="the correlation model (string) to use for isolation scale factors "
"Supported models: SIMPLIFIED (default), FULL, TOTAL, TOYS")
self.addOption ('correlationModelReco', 'SIMPLIFIED', type=str,
info="the correlation model (string) to use for reconstruction scale factors "
"Supported models: SIMPLIFIED (default), FULL, TOTAL, TOYS")
def makeAlgs (self, config) :
log = logging.getLogger('ElectronWorkingPointConfig')
if self.forceFullSimConfig:
log.warning("You are running ElectronWorkingPointConfig forcing full sim config")
log.warning("This is only intended to be used for testing purposes")
selectionPostfix = self.selectionName
if selectionPostfix != '' and selectionPostfix[0] != '_' :
selectionPostfix = '_' + selectionPostfix
# The setup below is inappropriate for Run 1
if config.geometry() is LHCPeriod.Run1:
raise ValueError ("Can't set up the ElectronWorkingPointConfig with %s, there must be something wrong!" % config.geometry().value)
postfix = self.postfix
if postfix is None :
postfix = self.selectionName
if postfix != '' and postfix[0] != '_' :
postfix = '_' + postfix
# Set up the track selection algorithm:
if self.trackSelection :
alg = config.createAlgorithm( 'CP::AsgLeptonTrackSelectionAlg',
'ElectronTrackSelectionAlg' + postfix,
reentrant=True )
alg.selectionDecoration = 'trackSelection' + postfix + ',as_bits'
alg.maxD0Significance = self.maxD0Significance
alg.maxDeltaZ0SinTheta = self.maxDeltaZ0SinTheta
alg.particles = config.readName (self.containerName)
alg.preselection = config.getPreselection (self.containerName, '')
if self.trackSelection :
config.addSelection (self.containerName, self.selectionName, alg.selectionDecoration,
preselection=self.addSelectionToPreselection)
if 'LH' in self.identificationWP:
# Set up the likelihood ID selection algorithm
# It is safe to do this before calibration, as the cluster E is used
alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronLikelihoodAlg' + postfix )
alg.selectionDecoration = 'selectLikelihood' + selectionPostfix + ',as_char'
if self.recomputeID:
# Rerun the likelihood ID
config.addPrivateTool( 'selectionTool', 'AsgElectronLikelihoodTool' )
alg.selectionTool.primaryVertexContainer = 'PrimaryVertices'
# Here we have to match the naming convention of EGSelectorConfigurationMapping.h
# which differ from the one used for scale factors
if config.geometry() >= LHCPeriod.Run3:
alg.selectionTool.WorkingPoint = self.identificationWP.replace("BLayer","BL") + 'Electron'
elif config.geometry() is LHCPeriod.Run2:
alg.selectionTool.WorkingPoint = self.identificationWP.replace("BLayer","BL") + 'Electron_Run2'
else:
# Select from Derivation Framework flags
config.addPrivateTool( 'selectionTool', 'CP::AsgFlagSelectionTool' )
dfFlag = "DFCommonElectronsLH" + self.identificationWP.split('LH')[0]
dfFlag = dfFlag.replace("BLayer","BL")
alg.selectionTool.selectionFlags = [dfFlag]
elif 'SiHit' in self.identificationWP:
# Only want SiHit electrons, so veto loose LH electrons
algVeto = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronLikelihoodAlgVeto' + postfix + 'Veto')
algVeto.selectionDecoration = 'selectLikelihoodVeto' + postfix + ',as_char'
config.addPrivateTool( 'selectionTool', 'CP::AsgFlagSelectionTool' )
algVeto.selectionTool.selectionFlags = ["DFCommonElectronsLHLoose"]
algVeto.selectionTool.invertFlags = [True]
algVeto.particles = config.readName (self.containerName)
algVeto.preselection = config.getPreselection (self.containerName, self.selectionName)
# add the veto as a selection
config.addSelection (self.containerName, self.selectionName, algVeto.selectionDecoration,
preselection=self.addSelectionToPreselection)
# Select SiHit electrons using IsEM bits
alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronLikelihoodAlg' + postfix )
alg.selectionDecoration = 'selectSiHit' + selectionPostfix + ',as_char'
# Select from Derivation Framework IsEM bits
config.addPrivateTool( 'selectionTool', 'CP::AsgMaskSelectionTool' )
dfVar = "DFCommonElectronsLHLooseBLIsEMValue"
alg.selectionTool.selectionVars = [dfVar]
mask = int( 0 | 0x1 << 1 | 0x1 << 2)
alg.selectionTool.selectionMasks = [mask]
elif 'DNN' in self.identificationWP:
if self.chargeIDSelectionRun2:
raise ValueError('DNN is not intended to be used with '
'`chargeIDSelectionRun2` option as there are '
'DNN WPs containing charge flip rejection.')
# Set up the DNN ID selection algorithm
alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronDNNAlg' + postfix )
alg.selectionDecoration = 'selectDNN' + selectionPostfix + ',as_char'
if self.recomputeID:
# Rerun the DNN ID
config.addPrivateTool( 'selectionTool', 'AsgElectronSelectorTool' )
# Here we have to match the naming convention of EGSelectorConfigurationMapping.h
if config.geometry() is LHCPeriod.Run3:
raise ValueError ( "DNN working points are not available for Run 3 yet.")
else:
alg.selectionTool.WorkingPoint = self.identificationWP + 'Electron'
else:
# Select from Derivation Framework flags
config.addPrivateTool( 'selectionTool', 'CP::AsgFlagSelectionTool' )
dfFlag = "DFCommonElectronsDNN" + self.identificationWP.split('DNN')[0]
alg.selectionTool.selectionFlags = [dfFlag]
alg.particles = config.readName (self.containerName)
alg.preselection = config.getPreselection (self.containerName, self.selectionName)
config.addSelection (self.containerName, self.selectionName, alg.selectionDecoration,
preselection=self.addSelectionToPreselection)
# maintain order of selections
if 'SiHit' in self.identificationWP:
# Set up the ElectronSiHitDecAlg algorithm to decorate SiHit electrons with a minimal amount of information:
algDec = config.createAlgorithm( 'CP::ElectronSiHitDecAlg', 'ElectronSiHitDecAlg' + postfix )
selDec = 'siHitEvtHasLeptonPair' + selectionPostfix + ',as_char'
algDec.selectionName = selDec.split(",")[0]
algDec.ElectronContainer = config.readName (self.containerName)
# Set flag to only collect SiHit electrons for events with an electron or muon pair to minimize size increase from SiHit electrons
algDec.RequireTwoLeptons = True
config.addSelection (self.containerName, self.selectionName, selDec,
preselection=self.addSelectionToPreselection)
# Set up the FSR selection
if self.doFSRSelection :
# save the flag set for the WP
wpFlag = alg.selectionDecoration.split(",")[0]
alg = config.createAlgorithm( 'CP::EgammaFSRForMuonsCollectorAlg', 'EgammaFSRForMuonsCollectorAlg' + postfix )
alg.selectionDecoration = wpFlag
alg.ElectronOrPhotonContKey = config.readName (self.containerName)
# For SiHit electrons, set flag to remove FSR electrons.
# For standard electrons, FSR electrons need to be added as they may be missed by the standard selection.
# For SiHit electrons FSR electrons are generally always selected, so they should be removed since they will be in the standard electron container.
if 'SiHit' in self.identificationWP:
alg.vetoFSR = True
# Set up the isolation selection algorithm:
if self.isolationWP != 'NonIso' :
alg = config.createAlgorithm( 'CP::EgammaIsolationSelectionAlg',
'ElectronIsolationSelectionAlg' + postfix )
alg.selectionDecoration = 'isolated' + selectionPostfix + ',as_char'
config.addPrivateTool( 'selectionTool', 'CP::IsolationSelectionTool' )
alg.selectionTool.ElectronWP = self.isolationWP
if self.closeByCorrection:
alg.selectionTool.IsoDecSuffix = "CloseByCorr"
alg.egammas = config.readName (self.containerName)
alg.preselection = config.getPreselection (self.containerName, self.selectionName)
config.addSelection (self.containerName, self.selectionName, alg.selectionDecoration,
preselection=self.addSelectionToPreselection)
if self.chargeIDSelectionRun2 and config.geometry() >= LHCPeriod.Run3:
log.warning("ECIDS is only available for Run 2 and will not have effect in run 3.")
# Select electrons only if they don't appear to have flipped their charge.
if self.chargeIDSelectionRun2 and config.geometry() < LHCPeriod.Run3:
alg = config.createAlgorithm( 'CP::AsgSelectionAlg',
'ElectronChargeIDSelectionAlg' + postfix )
alg.selectionDecoration = 'chargeID' + selectionPostfix + ',as_char'
if self.recomputeChargeID:
# Rerun the ECIDS BDT
config.addPrivateTool( 'selectionTool',
'AsgElectronChargeIDSelectorTool' )
alg.selectionTool.TrainingFile = \
'ElectronPhotonSelectorTools/ChargeID/ECIDS_20180731rel21Summer2018.root'
alg.selectionTool.WorkingPoint = 'Loose'
alg.selectionTool.CutOnBDT = -0.337671 # Loose 97%
else:
# Select from Derivation Framework flags
config.addPrivateTool( 'selectionTool', 'CP::AsgFlagSelectionTool' )
alg.selectionTool.selectionFlags = ["DFCommonElectronsECIDS"]
alg.particles = config.readName (self.containerName)
alg.preselection = config.getPreselection (self.containerName, self.selectionName)
config.addSelection (self.containerName, self.selectionName, alg.selectionDecoration,
preselection=self.addSelectionToPreselection)
correlationModels = ["SIMPLIFIED", "FULL", "TOTAL", "TOYS"]
sfList = []
# Set up the RECO electron efficiency correction algorithm:
if config.dataType() is not DataType.Data and not self.noEffSF:
if config.geometry() is LHCPeriod.Run2:
raise ValueError('Run 2 does not yet have efficiency correction, '
'please disable it by setting `noEffSF` to True.')
if 'DNN' in self.identificationWP:
raise ValueError('DNN does not yet have efficiency correction, '
'please disable it by setting `noEffSF` to True.')
alg = config.createAlgorithm( 'CP::ElectronEfficiencyCorrectionAlg',
'ElectronEfficiencyCorrectionAlgReco' + postfix )
config.addPrivateTool( 'efficiencyCorrectionTool',
'AsgElectronEfficiencyCorrectionTool' )
alg.scaleFactorDecoration = 'el_reco_effSF' + selectionPostfix + '_%SYS%'
alg.efficiencyCorrectionTool.RecoKey = "Reconstruction"
if self.correlationModelReco not in correlationModels:
raise ValueError('Invalid correlation model for reconstruction efficiency, '
f'has to be one of: {", ".join(correlationModels)}')
if config.geometry() >= LHCPeriod.Run3 and self.correlationModelReco != "TOTAL":
log.warning("Only TOTAL correlation model is currently supported "
"for reconstruction efficiency correction in Run 3.")
alg.efficiencyCorrectionTool.CorrelationModel = "TOTAL"
else:
alg.efficiencyCorrectionTool.CorrelationModel = self.correlationModelReco
if config.dataType() is DataType.FastSim:
alg.efficiencyCorrectionTool.ForceDataType = (
PATCore.ParticleDataType.Full if self.forceFullSimConfig
else PATCore.ParticleDataType.Fast)
elif config.dataType() is DataType.FullSim:
alg.efficiencyCorrectionTool.ForceDataType = \
PATCore.ParticleDataType.Full
alg.outOfValidity = 2 #silent
alg.outOfValidityDeco = 'el_reco_bad_eff' + selectionPostfix
alg.electrons = config.readName (self.containerName)
alg.preselection = config.getPreselection (self.containerName, self.selectionName)
if self.saveDetailedSF:
config.addOutputVar (self.containerName, alg.scaleFactorDecoration,
'reco_effSF' + postfix)
sfList += [alg.scaleFactorDecoration]
# Set up the ID electron efficiency correction algorithm:
if config.dataType() is not DataType.Data and not self.noEffSF:
alg = config.createAlgorithm( 'CP::ElectronEfficiencyCorrectionAlg',
'ElectronEfficiencyCorrectionAlgID' + postfix )
config.addPrivateTool( 'efficiencyCorrectionTool',
'AsgElectronEfficiencyCorrectionTool' )
alg.scaleFactorDecoration = 'el_id_effSF' + selectionPostfix + '_%SYS%'
alg.efficiencyCorrectionTool.IdKey = self.identificationWP.replace("LH","")
if self.correlationModelId not in correlationModels:
raise ValueError('Invalid correlation model for identification efficiency, '
f'has to be one of: {", ".join(correlationModels)}')
alg.efficiencyCorrectionTool.CorrelationModel = self.correlationModelId
if config.dataType() is DataType.FastSim:
alg.efficiencyCorrectionTool.ForceDataType = (
PATCore.ParticleDataType.Full if self.forceFullSimConfig
else PATCore.ParticleDataType.Fast)
elif config.dataType() is DataType.FullSim:
alg.efficiencyCorrectionTool.ForceDataType = \
PATCore.ParticleDataType.Full
alg.outOfValidity = 2 #silent
alg.outOfValidityDeco = 'el_id_bad_eff' + selectionPostfix
alg.electrons = config.readName (self.containerName)
alg.preselection = config.getPreselection (self.containerName, self.selectionName)
if self.saveDetailedSF:
config.addOutputVar (self.containerName, alg.scaleFactorDecoration,
'id_effSF' + postfix)
sfList += [alg.scaleFactorDecoration]
# Set up the ISO electron efficiency correction algorithm:
if config.dataType() is not DataType.Data and self.isolationWP != 'NonIso' and not self.noEffSF:
alg = config.createAlgorithm( 'CP::ElectronEfficiencyCorrectionAlg',
'ElectronEfficiencyCorrectionAlgIsol' + postfix )
config.addPrivateTool( 'efficiencyCorrectionTool',
'AsgElectronEfficiencyCorrectionTool' )
alg.scaleFactorDecoration = 'el_isol_effSF' + selectionPostfix + '_%SYS%'
alg.efficiencyCorrectionTool.IdKey = self.identificationWP.replace("LH","")
alg.efficiencyCorrectionTool.IsoKey = self.isolationWP
if self.correlationModelIso not in correlationModels:
raise ValueError('Invalid correlation model for isolation efficiency, '
f'has to be one of: {", ".join(correlationModels)}')
if config.geometry() >= LHCPeriod.Run3:
log.warning("Only TOTAL correlation model is currently supported "
"for isolation efficiency correction in Run 3.")
alg.efficiencyCorrectionTool.CorrelationModel = "TOTAL"
else:
alg.efficiencyCorrectionTool.CorrelationModel = self.correlationModelIso
if config.dataType() is DataType.FastSim:
alg.efficiencyCorrectionTool.ForceDataType = (
PATCore.ParticleDataType.Full if self.forceFullSimConfig
else PATCore.ParticleDataType.Fast)
elif config.dataType() is DataType.FullSim:
alg.efficiencyCorrectionTool.ForceDataType = \
PATCore.ParticleDataType.Full
alg.outOfValidity = 2 #silent
alg.outOfValidityDeco = 'el_isol_bad_eff' + selectionPostfix
alg.electrons = config.readName (self.containerName)
alg.preselection = config.getPreselection (self.containerName, self.selectionName)
if self.saveDetailedSF:
config.addOutputVar (self.containerName, alg.scaleFactorDecoration,
'isol_effSF' + postfix)
sfList += [alg.scaleFactorDecoration]
# TO-DO: add trigger SFs, for which we need ID key + ISO key + Trigger key !
if self.chargeIDSelectionRun2:
# ECIDS is currently not supported in R22.
# SFs might become available or it will be part of the DNN ID.
pass
if config.dataType() is not DataType.Data and not self.noEffSF and self.saveCombinedSF:
alg = config.createAlgorithm( 'CP::AsgObjectScaleFactorAlg',
'ElectronCombinedEfficiencyScaleFactorAlg' + postfix )
alg.particles = config.readName (self.containerName)
alg.inScaleFactors = sfList
alg.outScaleFactor = 'effSF' + postfix + '_%SYS%'
config.addOutputVar (self.containerName, alg.outScaleFactor, 'effSF' + postfix)
class ElectronTriggerAnalysisSFBlock (ConfigBlock):
def __init__ (self, configName='') :
super (ElectronTriggerAnalysisSFBlock, self).__init__ ()
self.addOption ('triggerChainsPerYear', {}, type=None,
info="a dictionary with key (string) the year and value (list of "
"strings) the trigger chains. The default is {} (empty dictionary).")
self.addOption ('electronID', '', type=str,
info="the electron ID WP (string) to use.")
self.addOption ('electronIsol', '', type=str,
info="the electron isolation WP (string) to use.")
self.addOption ('saveEff', False, type=bool,
info="define whether we decorate also the trigger scale efficiency "
"The default is false.")
self.addOption ('prefixSF', 'trigEffSF', type=str,
info="the decoration prefix for trigger scale factors, "
"the default is 'trigEffSF'")
self.addOption ('prefixEff', 'trigEff', type=str,
info="the decoration prefix for MC trigger efficiencies, "
"the default is 'trigEff'")
self.addOption ('includeAllYears', False, type=bool,
info="if True, all configured years will be included in all jobs. "
"The default is False.")
self.addOption ('removeHLTPrefix', True, type=bool,
info="remove the HLT prefix from trigger chain names, "
"The default is True.")
self.addOption ('useToolKeyAsOutput', False, type=bool,
info="use tool trigger key as output, "
"The default is False.")
self.addOption ('containerName', '', type=str,
info="the input electron container, with a possible selection, in "
"the format container or container.selection.")
def makeAlgs (self, config) :
if config.dataType() is not DataType.Data:
log = logging.getLogger('ElectronTriggerSFConfig')
if self.includeAllYears and not self.useToolKeyAsOutput:
log.warning('`includeAllYears` is set to True, but `useToolKeyAsOutput` is set to False. '
'This will cause multiple branches to be written out with the same content.')
# Dictionary from TrigGlobalEfficiencyCorrection/Triggers.cfg
# Key is trigger chain (w/o HLT prefix)
# Value is empty for single leg trigger or list of legs
triggerDict = TriggerDict()
# currently recommended versions
version_Run2 = "2015_2018/rel21.2/Precision_Summer2020_v1"
version_Run3 = "2015_2025/rel22.2/2022_Summer_Prerecom_v1"
version = version_Run2 if config.geometry() is LHCPeriod.Run2 else version_Run3
# Dictionary from TrigGlobalEfficiencyCorrection/MapKeys.cfg
# Key is year_leg
# Value is list of configs available, first one will be used
mapKeysDict = MapKeysDict(version)
# helper function for leg filtering, very hardcoded but allows autoconfiguration
def filterConfFromMap(conf, electronMapKeys):
if not conf:
raise ValueError("No configuration found for trigger chain.")
if len(conf) == 1:
return conf[0]
for c in conf:
if c in electronMapKeys:
return c
return conf[0]
if self.includeAllYears:
years = [int(year) for year in self.triggerChainsPerYear.keys()]
if any(year in years for year in [2015, 2016, 2017, 2018]) \
and any(year in years for year in [2022, 2023, 2024, 2025]):
raise ValueError("Mixing years from Run 2 and Run 3 in the same job is currently not supported.")
elif config.campaign() is Campaign.MC20a:
years = [2015, 2016]
elif config.campaign() is Campaign.MC20d:
years = [2017]
elif config.campaign() is Campaign.MC20e:
years = [2018]
elif config.campaign() in [Campaign.MC21a, Campaign.MC23a]:
years = [2022]
elif config.campaign() in [Campaign.MC23c, Campaign.MC23d]:
years = [2023]
# prepare keys
import ROOT
triggerChainsPerYear_Run2 = {}
triggerChainsPerYear_Run3 = {}
for year, chains in self.triggerChainsPerYear.items():
if not chains:
log.warning("No trigger chains configured for year %s. "
"Assuming this is intended, no Electron trigger SF will be computed.", year)
continue
chains_split = [chain.replace("HLT_", "").replace(" || ", "_OR_") for chain in chains]
if int(year) >= 2022:
triggerChainsPerYear_Run3[str(year)] = ' || '.join(chains_split)
else:
triggerChainsPerYear_Run2[str(year)] = ' || '.join(chains_split)
electronMapKeys_Run2 = ROOT.std.map("string", "string")()
electronMapKeys_Run3 = ROOT.std.map("string", "string")()
sc_Run2 = ROOT.TrigGlobalEfficiencyCorrectionTool.suggestElectronMapKeys(triggerChainsPerYear_Run2, version_Run2, electronMapKeys_Run2)
sc_Run3 = ROOT.TrigGlobalEfficiencyCorrectionTool.suggestElectronMapKeys(triggerChainsPerYear_Run3, version_Run3, electronMapKeys_Run3)
if sc_Run2.code() != 2 or sc_Run3.code() != 2:
raise RuntimeError("Failed to suggest electron map keys")
electronMapKeys = dict(electronMapKeys_Run2) | dict(electronMapKeys_Run3)
# collect configurations
triggerConfigs = {}
for year in years:
triggerChains = self.triggerChainsPerYear.get(int(year), self.triggerChainsPerYear.get(str(year), []))
for chain in triggerChains:
chain = chain.replace(" || ", "_OR_")
chain_noHLT = chain.replace("HLT_", "")
chain_out = chain_noHLT if self.removeHLTPrefix else chain
legs = triggerDict[chain_noHLT]
if not legs:
if chain_noHLT[0] == 'e' and chain_noHLT[1].isdigit:
chain_key = f"{year}_{chain_noHLT}"
chain_conf = mapKeysDict[chain_key][0]
triggerConfigs[chain_conf if self.useToolKeyAsOutput else chain_out] = chain_conf
else:
for leg in legs:
if leg[0] == 'e' and leg[1].isdigit:
leg_out = leg if self.removeHLTPrefix else f"HLT_{leg}"
leg_key = f"{year}_{leg}"
leg_conf = filterConfFromMap(mapKeysDict[leg_key], electronMapKeys)
triggerConfigs[leg_conf if self.useToolKeyAsOutput else leg_out] = leg_conf
decorations = [self.prefixSF]
if self.saveEff:
decorations += [self.prefixEff]
for label, conf in triggerConfigs.items():
for deco in decorations:
alg = config.createAlgorithm('CP::ElectronEfficiencyCorrectionAlg',
'EleTrigEfficiencyCorrectionsAlg' + deco +
'_' + label)
config.addPrivateTool( 'efficiencyCorrectionTool',
'AsgElectronEfficiencyCorrectionTool' )
# Reproduce config from TrigGlobalEfficiencyAlg
alg.efficiencyCorrectionTool.MapFilePath = "ElectronEfficiencyCorrection/" + version + "/map4.txt"
alg.efficiencyCorrectionTool.IdKey = self.electronID.replace("LH","")
alg.efficiencyCorrectionTool.IsoKey = self.electronIsol
alg.efficiencyCorrectionTool.TriggerKey = (
("Eff_" if deco == self.prefixEff else "") + conf)
alg.efficiencyCorrectionTool.CorrelationModel = "TOTAL"
alg.efficiencyCorrectionTool.ForceDataType = \
PATCore.ParticleDataType.Full
alg.scaleFactorDecoration = f"el_{deco}_{label}_%SYS%"
alg.outOfValidity = 2 #silent
alg.outOfValidityDeco = f"bad_eff_ele{deco}_{label}"
alg.electrons = config.readName (self.containerName)
alg.preselection = config.getPreselection (self.containerName, "")
config.addOutputVar (self.containerName, alg.scaleFactorDecoration, f"{deco}_{label}")
class ElectronLRTMergedConfig (ConfigBlock) :
def __init__ (self) :
super (ElectronLRTMergedConfig, self).__init__ ()
self.addOption (
'inputElectrons', 'Electrons', type=str,
noneAction='error',
info="the name of the input electron container."
)
self.addOption (
'inputLRTElectrons', 'LRTElectrons', type=str,
noneAction='error',
info="the name of the input LRT electron container."
)
self.addOption (
'containerName', 'Electrons_LRTMerged', type=str,
noneAction='error',
info="the name of the output container after LRT merging."
)
def makeAlgs (self, config) :
if config.isPhyslite() :
raise(RuntimeError("Electron LRT merging is not available in Physlite mode"))
alg = config.createAlgorithm( "CP::ElectronLRTMergingAlg", "ElectronLRTMergingAlg" + self.containerName )
alg.PromptElectronLocation = self.inputElectrons
alg.LRTElectronLocation = self.inputLRTElectrons
alg.OutputCollectionName = self.containerName
alg.CreateViewCollection = False