Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
ElectronAnalysisConfig.py 35.02 KiB
# Copyright (C) 2002-2024 CERN for the benefit of the ATLAS collaboration

# AnaAlgorithm import(s):
from AnalysisAlgorithmsConfig.ConfigBlock import ConfigBlock
from AthenaConfiguration.Enums import LHCPeriod
from AnalysisAlgorithmsConfig.ConfigAccumulator import DataType
from TrigGlobalEfficiencyCorrection.TriggerLeg_DictHelpers import TriggerDict, MapKeysDict
from Campaigns.Utils import Campaign

# 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 ('containerName', containerName, type=str,
            noneAction='error',
            info="the name of the output container after calibration.")
        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.5e3, type=float,
            info="the minimum pT cut to apply to calibrated electrons. "
            "The default is 4.5 GeV.")
        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")


    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"""
        # Set up the calibration and smearing algorithm:
        alg = config.createAlgorithm( 'CP::EgammaCalibrationAndSmearingAlg', name + self.postfix )
        config.addPrivateTool( 'calibrationAndSmearingTool',
                            'CP::EgammaCalibrationAndSmearingTool' )
        alg.calibrationAndSmearingTool.ESModel = 'es2022_R22_PRE'
        alg.calibrationAndSmearingTool.decorrelationModel = '1NP_v1'
        alg.calibrationAndSmearingTool.useFastSim = (
            0 if self.forceFullSimConfig
            else int( config.dataType() is DataType.FastSim ))
        alg.egammas = config.readName (self.containerName)
        alg.egammasOut = config.copyName (self.containerName)
        alg.preselection = config.getPreselection (self.containerName, '')
        return alg


    def makeAlgs (self, config) :

        if self.forceFullSimConfig:
            print("WARNING! You are running ElectronCalibrationConfig forcing full sim config")
            print("WARNING! This is only intended to be used for testing purposes")

        if config.isPhyslite() :
            config.setSourceName (self.containerName, "AnalysisElectrons")
        else :
            config.setSourceName (self.containerName, "Electrons")

        # 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 = 2.47
        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

        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
        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)



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 ('writeTrackD0Z0', False, type = bool,
            info="save the d0 significance and z0sinTheta variables so they can be written out")
        self.addOption ('likelihoodWP', None, type=str,
            info="the ID WP (string) to use. Supported ID WPs: TightLH, "
            "MediumLH, LooseBLayerLH. ")
        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 ('recomputeLikelihood', False, type=bool,
            info="whether to rerun the LH. The default is False, i.e. to use "
            "derivation flags.")
        self.addOption ('chargeIDSelection', False, type=bool,
            info="whether to run the ECIDS tool. The default is False.")
        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 ('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.")


    def makeAlgs (self, config) :

        if self.forceFullSimConfig:
            print("WARNING! You are running ElectronWorkingPointConfig forcing full sim config")
            print("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.writeTrackD0Z0 or self.trackSelection :
            alg = config.createAlgorithm( 'CP::AsgLeptonTrackSelectionAlg',
                                        'ElectronTrackSelectionAlg' + postfix )
            alg.selectionDecoration = 'trackSelection' + postfix + ',as_bits'
            alg.maxD0Significance = self.maxD0Significance
            alg.maxDeltaZ0SinTheta = self.maxDeltaZ0SinTheta 
            alg.decorateTTVAVars = self.writeTrackD0Z0
            alg.particles = config.readName (self.containerName)
            alg.preselection = config.getPreselection (self.containerName, '')
            if self.trackSelection : 
                config.addSelection (self.containerName, self.selectionName, alg.selectionDecoration)
            if self.writeTrackD0Z0 :
                alg.d0sigDecoration = 'd0sig' + postfix
                alg.z0sinthetaDecoration = 'z0sintheta' + postfix
                config.addOutputVar (self.containerName, alg.d0sigDecoration, alg.d0sigDecoration,noSys=True)
                config.addOutputVar (self.containerName, alg.z0sinthetaDecoration, alg.z0sinthetaDecoration,noSys=True)
                
        if 'LH' in self.likelihoodWP:
            # 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_bits'
            if self.recomputeLikelihood:
                # 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.likelihoodWP.replace("BLayer","BL") + 'Electron'
                elif config.geometry() is LHCPeriod.Run2:
                    alg.selectionTool.WorkingPoint = self.likelihoodWP.replace("BLayer","BL") + 'Electron_Run2'
            else:
                # Select from Derivation Framework flags
                config.addPrivateTool( 'selectionTool', 'CP::AsgFlagSelectionTool' )
                dfFlag = "DFCommonElectronsLH" + self.likelihoodWP.split('LH')[0]
                dfFlag = dfFlag.replace("BLayer","BL")
                alg.selectionTool.selectionFlags = [dfFlag]
        elif 'SiHit' in self.likelihoodWP:
            # Only want SiHit electrons, so veto loose LH electrons
            algVeto = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronLikelihoodAlgVeto' + postfix + 'Veto')
            algVeto.selectionDecoration = 'selectLikelihoodVeto' + postfix + ',as_bits'  
            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 in as preselection a veto
            config.addSelection (self.containerName, self.selectionName, algVeto.selectionDecoration)

            # Select SiHit electrons using IsEM bits
            alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronLikelihoodAlg' + postfix )
            alg.selectionDecoration = 'selectSiHit' + selectionPostfix + ',as_bits'
            # 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]

            # 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_bits'
            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)
        elif 'DNN' in self.likelihoodWP:
            # Set up the DNN ID selection algorithm
            alg = config.createAlgorithm( 'CP::AsgSelectionAlg', 'ElectronDNNAlg' + postfix )
            alg.selectionDecoration = 'selectDNN' + selectionPostfix + ',as_bits'
            if self.recomputeLikelihood:
                # 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.likelihoodWP + 'Electron'
            else:
                # Select from Derivation Framework flags
                raise ValueError ( "DNN working points are not available in derivations yet.")

        alg.particles = config.readName (self.containerName)
        alg.preselection = config.getPreselection (self.containerName, self.selectionName)
        config.addSelection (self.containerName, self.selectionName, alg.selectionDecoration)

        # 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.likelihoodWP:
                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_bits'
            config.addPrivateTool( 'selectionTool', 'CP::IsolationSelectionTool' )
            alg.selectionTool.ElectronWP = self.isolationWP
            alg.egammas = config.readName (self.containerName)
            alg.preselection = config.getPreselection (self.containerName, self.selectionName)
            config.addSelection (self.containerName, self.selectionName, alg.selectionDecoration)

        # Select electrons only if they don't appear to have flipped their charge.
        if self.chargeIDSelection:
            alg = config.createAlgorithm( 'CP::AsgSelectionAlg',
                                          'ElectronChargeIDSelectionAlg' + postfix )
            alg.selectionDecoration = 'chargeID' + selectionPostfix + ',as_bits'
            config.addPrivateTool( 'selectionTool',
                                   'AsgElectronChargeIDSelectorTool' )
            alg.selectionTool.TrainingFile = \
                'ElectronPhotonSelectorTools/ChargeID/ECIDS_20180731rel21Summer2018.root'
            alg.selectionTool.WorkingPoint = 'Loose'
            alg.selectionTool.CutOnBDT = -0.337671 # Loose 97%
            alg.particles = config.readName (self.containerName)
            alg.preselection = config.getPreselection (self.containerName, self.selectionName)
            config.addSelection (self.containerName, self.selectionName, alg.selectionDecoration)

        # Set up the RECO electron efficiency correction algorithm:
        if config.dataType() is not DataType.Data and not self.noEffSF:
            alg = config.createAlgorithm( 'CP::ElectronEfficiencyCorrectionAlg',
                                          'ElectronEfficiencyCorrectionAlgReco' + postfix )
            config.addPrivateTool( 'efficiencyCorrectionTool',
                                   'AsgElectronEfficiencyCorrectionTool' )
            alg.scaleFactorDecoration = 'el_reco_effSF' + selectionPostfix + '_%SYS%'
            alg.efficiencyCorrectionTool.RecoKey = "Reconstruction"
            alg.efficiencyCorrectionTool.CorrelationModel = "TOTAL"
            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
            if config.geometry() is LHCPeriod.Run2:
                alg.efficiencyCorrectionTool.MapFilePath = "ElectronEfficiencyCorrection/2015_2018/rel21.2/Precision_Summer2020_v1/map4.txt"
            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)
            config.addOutputVar (self.containerName, alg.scaleFactorDecoration, 'reco_effSF' + postfix)

        # 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.likelihoodWP.replace("LH","")
            alg.efficiencyCorrectionTool.CorrelationModel = "TOTAL"
            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
            if config.geometry() is LHCPeriod.Run2:
                alg.efficiencyCorrectionTool.MapFilePath = "ElectronEfficiencyCorrection/2015_2018/rel21.2/Precision_Summer2020_v1/map4.txt"
            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)
            config.addOutputVar (self.containerName, alg.scaleFactorDecoration, 'id_effSF' + postfix)

        # 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.likelihoodWP.replace("LH","")
            alg.efficiencyCorrectionTool.IsoKey = self.isolationWP
            alg.efficiencyCorrectionTool.CorrelationModel = "TOTAL"
            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
            if config.geometry() is LHCPeriod.Run2:
                alg.efficiencyCorrectionTool.MapFilePath = "ElectronEfficiencyCorrection/2015_2018/rel21.2/Precision_Summer2020_v1/map4.txt"
                alg.efficiencyCorrectionTool.CorrelationModel = "SIMPLIFIED" # remove when Run 2 R25 recommendations are available!
            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)
            config.addOutputVar (self.containerName, alg.scaleFactorDecoration, 'isol_effSF' + postfix)

        # TO-DO: add trigger SFs, for which we need ID key + ISO key + Trigger key !

        if self.chargeIDSelection:
            # ECIDS is currently not supported in R22.
            # SFs might become available or it will be part of the DNN ID.
            pass


def makeElectronCalibrationConfig( seq, containerName, postfix = None,
                                   crackVeto = None,
                                   isolationCorrection = None,
                                   forceFullSimConfig = None):
    """Create electron calibration configuration blocks

    This makes all the algorithms that need to be run first befor
    all working point specific algorithms and that can be shared
    between the working points.

    Keyword arguments:
      postfix -- a postfix to apply to decorations and algorithm
                 names.  this is mostly used/needed when using this
                 sequence with multiple working points to ensure all
                 names are unique.
      isolationCorrection -- Whether or not to perform isolation correction
      forceFullSimConfig -- imposes full-sim config for FastSim for testing
    """

    config = ElectronCalibrationConfig (containerName)
    config.setOptionValue ('crackVeto', crackVeto)
    config.setOptionValue ('isolationCorrection', isolationCorrection)
    config.setOptionValue ('forceFullSimConfig', forceFullSimConfig)
    seq.append (config)





def makeElectronWorkingPointConfig( seq, containerName, workingPoint,
                                    selectionName,
                                    recomputeLikelihood = None,
                                    chargeIDSelection = None,
                                    noEffSF = None,
                                    forceFullSimConfig = None):
    """Create electron analysis configuration blocks

    Keyword arguments:
      workingPoint -- The working point to use
      selectionName -- a postfix to apply to decorations and algorithm
                 names.  this is mostly used/needed when using this
                 sequence with multiple working points to ensure all
                 names are unique.
      recomputeLikelihood -- Whether to rerun the LH. If not, use derivation flags
      chargeIDSelection -- Whether or not to perform charge ID/flip selection
      noEffSF -- Disables the calculation of efficiencies and scale factors
      forceFullSimConfig -- imposes full-sim config for FastSim for testing
    """


    config = ElectronWorkingPointConfig (containerName, selectionName)
    if workingPoint is not None :
        splitWP = workingPoint.split ('.')
        if len (splitWP) != 2 :
            raise ValueError ('working point should be of format "likelihood.isolation", not ' + workingPoint)
        config.setOptionValue ('likelihoodWP', splitWP[0])
        config.setOptionValue ('isolationWP', splitWP[1])
    config.setOptionValue ('recomputeLikelihood', recomputeLikelihood)
    config.setOptionValue ('chargeIDSelection', chargeIDSelection)
    config.setOptionValue ('noEffSF', noEffSF)
    config.setOptionValue ('forceFullSimConfig', forceFullSimConfig)
    seq.append (config)


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 ('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:

            # 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()

            version = ("2015_2018/rel21.2/Precision_Summer2020_v1"
                       if config.geometry() is LHCPeriod.Run2 else
                       "2015_2025/rel22.2/2022_Summer_Prerecom_v1")
            # Dictionary from TrigGlobalEfficiencyCorrection/MapKeys.cfg
            # Key is year_leg
            # Value is list of configs available, first one will be used
            mapKeysDict = MapKeysDict(version)

            if 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']

            triggerConfigs = {}
            for year in years:
                triggerChains = self.triggerChainsPerYear.get(year,[])
                for chain in triggerChains:
                    chain = chain.replace("HLT_", "").replace(" || ", "_OR_")
                    legs = triggerDict[chain]
                    if len(legs)==0:
                        if chain[0]=='e' and chain[1].isdigit:
                            triggerConfigs[chain] = mapKeysDict[year + '_' + chain]
                    else:
                        for leg in legs:
                            if leg[0]=='e' and leg[1].isdigit:
                                triggerConfigs[leg] = mapKeysDict[year + '_' + leg]

            decorations = ['EffSF']
            if self.saveEff:
                decorations += ['Eff']

            for trig, conf in triggerConfigs.items():
                for deco in decorations:
                    alg = config.createAlgorithm('CP::ElectronEfficiencyCorrectionAlg',
                                                 'EleTrigEfficiencyCorrectionsAlg' + deco +
                                                 '_' + trig)
                    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 "SF" not in deco else "") + conf[0])
                    alg.efficiencyCorrectionTool.CorrelationModel = "TOTAL"
                    alg.efficiencyCorrectionTool.ForceDataType = \
                        PATCore.ParticleDataType.Full

                    alg.scaleFactorDecoration = (
                        'el_trig' + deco + '_' + trig + '_%SYS%')
                    alg.outOfValidity = 2 #silent
                    alg.outOfValidityDeco = 'bad_eff_eletrig' + deco + '_' + trig
                    alg.electrons = config.readName (self.containerName)
                    alg.preselection = config.getPreselection (self.containerName, '')
                    config.addOutputVar (self.containerName, alg.scaleFactorDecoration, 'trig' + deco + '_' + trig)