diff --git a/PhysicsAnalysis/Algorithms/AnalysisAlgorithmsConfig/data/example_config.yaml b/PhysicsAnalysis/Algorithms/AnalysisAlgorithmsConfig/data/example_config.yaml index 765d70983f977f4fc423df3f07383a2d8ef58564..8d03716d932064e0f51a1c303fc24577f80f0f9f 100644 --- a/PhysicsAnalysis/Algorithms/AnalysisAlgorithmsConfig/data/example_config.yaml +++ b/PhysicsAnalysis/Algorithms/AnalysisAlgorithmsConfig/data/example_config.yaml @@ -129,6 +129,7 @@ Trigger: photons: 'AnaPhotons' muons: 'AnaMuons' + # After configuring each container, many variables will be saved automatically. Output: treeName: 'analysis' diff --git a/PhysicsAnalysis/Algorithms/AnalysisAlgorithmsConfig/python/ConfigFactory.py b/PhysicsAnalysis/Algorithms/AnalysisAlgorithmsConfig/python/ConfigFactory.py index 308a0cd4be806f643609e61e8673dd55196db10c..6829656c7abef2f016a849b86cd4a098b2891dc4 100644 --- a/PhysicsAnalysis/Algorithms/AnalysisAlgorithmsConfig/python/ConfigFactory.py +++ b/PhysicsAnalysis/Algorithms/AnalysisAlgorithmsConfig/python/ConfigFactory.py @@ -224,6 +224,10 @@ class ConfigFactory(): from AsgAnalysisAlgorithms.EventCleaningConfig import EventCleaningBlock self.addAlgConfigBlock(algName="EventCleaning", alg=EventCleaningBlock) + # trigger + from TriggerAnalysisAlgorithms.TriggerAnalysisConfig import Trigger + self.addAlgConfigBlock(algName="Trigger", alg=Trigger) + # jets from JetAnalysisAlgorithms.JetAnalysisConfig import makeJetAnalysisConfig self.addAlgConfigBlock(algName="Jets", alg=makeJetAnalysisConfig) @@ -301,11 +305,6 @@ class ConfigFactory(): from AsgAnalysisAlgorithms.AsgAnalysisConfig import ObjectCutFlowBlock self.addAlgConfigBlock(algName='ObjectCutFlow', alg=ObjectCutFlowBlock) - # trigger - from TriggerAnalysisAlgorithms.TriggerAnalysisConfig import TriggerAnalysisBlock - self.addAlgConfigBlock(algName="Trigger", alg=TriggerAnalysisBlock, - defaults={'configName': 'Trigger'}) - # event selection from EventSelectionAlgorithms.EventSelectionConfig import makeMultipleEventSelectionConfigs self.addAlgConfigBlock(algName='EventSelection', alg=makeMultipleEventSelectionConfigs) diff --git a/PhysicsAnalysis/Algorithms/TriggerAnalysisAlgorithms/python/TriggerAnalysisConfig.py b/PhysicsAnalysis/Algorithms/TriggerAnalysisAlgorithms/python/TriggerAnalysisConfig.py index 50140407884cc01b92f520db6bbaa28357c62ba3..b7c7d3c58b83f98fb56a395a247ac8a992176606 100644 --- a/PhysicsAnalysis/Algorithms/TriggerAnalysisAlgorithms/python/TriggerAnalysisConfig.py +++ b/PhysicsAnalysis/Algorithms/TriggerAnalysisAlgorithms/python/TriggerAnalysisConfig.py @@ -1,10 +1,10 @@ -# Copyright (C) 2002-2023 CERN for the benefit of the ATLAS collaboration +# Copyright (C) 2002-2024 CERN for the benefit of the ATLAS collaboration # AnaAlgorithm import(s): from AnalysisAlgorithmsConfig.ConfigBlock import ConfigBlock +from AnalysisAlgorithmsConfig.ConfigSequence import groupBlocks from AnalysisAlgorithmsConfig.ConfigAccumulator import DataType from AthenaConfiguration.Enums import LHCPeriod -from Campaigns.Utils import Campaign class TriggerAnalysisBlock (ConfigBlock): @@ -32,32 +32,6 @@ class TriggerAnalysisBlock (ConfigBlock): # TODO: add info string self.addOption ('noL1', False, type=bool, info="") - 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 ('photonIsol', '', type=str, - info="the photon isolation WP (string) to use.") - self.addOption ('muonID', '', type=str, - info="the muon quality WP (string) to use.") - self.addOption ('electrons', '', type=str, - info="the input electron container, with a possible selection, in " - "the format container or container.selection.") - self.addOption ('muons', '', type=str, - info="the input muon container, with a possible selection, in the " - "format container or container.selection.") - self.addOption ('photons', '', type=str, - info="the input photon container, with a possible selection, in " - "the format container or container.selection.") - 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. Still performs the global trigger " - "matching (same behaviour as on data). The default is False.") - self.addOption ('noGlobalTriggerEff', False, type=bool, - info="disables the global trigger efficiency tool (including " - "matching), which is only suited for electron/muon/photon " - "trigger legs. The default is False.") def makeTriggerDecisionTool(self, config): @@ -72,22 +46,6 @@ class TriggerAnalysisBlock (ConfigBlock): return decisionTool - def makeTriggerMatchingTool(self, config, decisionTool): - - # Create public trigger tools - drScoringTool = config.createPublicTool( 'Trig::DRScoringTool', 'DRScoringTool' ) - if config.geometry() == LHCPeriod.Run3: - matchingTool = config.createPublicTool( 'Trig::R3MatchingTool', 'MatchingTool' ) - matchingTool.ScoringTool = '%s/%s' % \ - ( drScoringTool.getType(), drScoringTool.getName() ) - matchingTool.TrigDecisionTool = '%s/%s' % \ - ( decisionTool.getType(), decisionTool.getName() ) - else: - matchingTool = config.createPublicTool( 'Trig::MatchFromCompositeTool', 'MatchingTool' ) - if config.isPhyslite(): - matchingTool.InputPrefix = "AnalysisTrigMatch_" - - return matchingTool def makeTriggerSelectionAlg(self, config, decisionTool): @@ -116,63 +74,9 @@ class TriggerAnalysisBlock (ConfigBlock): alg.prescaleDecoration = 'prescale' return - - def makeTriggerGlobalEffCorrAlg(self, config, matchingTool, noSF): - - alg = config.createAlgorithm( 'CP::TrigGlobalEfficiencyAlg', 'TrigGlobalSFAlg' ) - if config.geometry() == LHCPeriod.Run3: - alg.triggers_2022 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in self.triggerChainsPerYear.get('2022',[])] - alg.triggers_2023 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in self.triggerChainsPerYear.get('2023',[])] - alg.triggers_2024 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in self.triggerChainsPerYear.get('2024',[])] - alg.triggers_2025 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in self.triggerChainsPerYear.get('2025',[])] - if config.campaign() in [Campaign.MC21a, Campaign.MC23a]: - if not alg.triggers_2022: - raise ValueError( 'TriggerAnalysisConfig: you must provide a set of triggers for the year 2022!' ) - elif config.campaign() is Campaign.MC23c: - if not alg.triggers_2023: - raise ValueError( 'TriggerAnalysisConfig: you must provide a set of triggers for the year 2023!' ) - else: - alg.triggers_2015 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in self.triggerChainsPerYear.get('2015',[])] - alg.triggers_2016 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in self.triggerChainsPerYear.get('2016',[])] - alg.triggers_2017 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in self.triggerChainsPerYear.get('2017',[])] - alg.triggers_2018 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in self.triggerChainsPerYear.get('2018',[])] - if config.campaign() is Campaign.MC20a: - if not (alg.triggers_2015 and alg.triggers_2016): - raise ValueError( 'TriggerAnalysisConfig: you must provide a set of triggers for the years 2015 and 2016!' ) - elif config.campaign() is Campaign.MC20d: - if not alg.triggers_2017: - raise ValueError( 'TriggerAnalysisConfig: you must provide a set of triggers for the year 2017!' ) - elif config.campaign() is Campaign.MC20e: - if not alg.triggers_2018: - raise ValueError( 'TriggerAnalysisConfig: you must provide a set of triggers for the year 2018!' ) - alg.matchingTool = '%s/%s' % ( matchingTool.getType(), matchingTool.getName() ) - alg.isRun3Geo = config.geometry() == LHCPeriod.Run3 - alg.scaleFactorDecoration = 'globalTriggerEffSF_%SYS%' - alg.matchingDecoration = 'globalTriggerMatch_%SYS%' - alg.eventDecisionOutputDecoration = 'globalTriggerMatch_dontsave_%SYS%' - alg.doMatchingOnly = config.dataType() is DataType.Data or noSF - alg.noFilter = self.noFilter - alg.electronID = self.electronID - alg.electronIsol = self.electronIsol - alg.photonIsol = self.photonIsol - alg.muonID = self.muonID - if self.electrons: - alg.electrons, alg.electronSelection = config.readNameAndSelection(self.electrons) - if self.muons: - alg.muons, alg.muonSelection = config.readNameAndSelection(self.muons) - if self.photons: - alg.photons, alg.photonSelection = config.readNameAndSelection(self.photons) - if not (self.electrons or self.muons or self.photons): - raise ValueError ('TriggerAnalysisConfig: at least one object collection must be provided! (electrons, muons, photons)' ) - - if config.dataType() != DataType.Data and not alg.doMatchingOnly: - config.addOutputVar ('EventInfo', alg.scaleFactorDecoration, 'globalTriggerEffSF') - config.addOutputVar ('EventInfo', alg.matchingDecoration, 'globalTriggerMatch', noSys=False) - - return + def makeAlgs (self, config) : - # if we are only given the trigger dictionary, we fill the selection list automatically if self.triggerChainsPerYear and not self.triggerChainsForSelection: triggers = set() @@ -188,14 +92,15 @@ class TriggerAnalysisBlock (ConfigBlock): # Create the decision algorithm, keeping track of the decision tool for later decisionTool = self.makeTriggerDecisionTool(config) - # Now pass it to the matching algorithm, keeping track of the matching tool for later - matchingTool = self.makeTriggerMatchingTool(config, decisionTool) - if self.triggerChainsForSelection: self.makeTriggerSelectionAlg(config, decisionTool) - # Calculate multi-lepton (electron/muon/photon) trigger efficiencies and SFs - if self.triggerChainsPerYear and not self.noGlobalTriggerEff: - self.makeTriggerGlobalEffCorrAlg(config, matchingTool, self.noEffSF) - return + + + +@groupBlocks +def Trigger(seq): + seq.append(TriggerAnalysisBlock()) + from TriggerAnalysisAlgorithms.TriggerAnalysisSFConfig import TriggerAnalysisSFBlock + seq.append(TriggerAnalysisSFBlock()) diff --git a/PhysicsAnalysis/Algorithms/TriggerAnalysisAlgorithms/python/TriggerAnalysisSFConfig.py b/PhysicsAnalysis/Algorithms/TriggerAnalysisAlgorithms/python/TriggerAnalysisSFConfig.py new file mode 100644 index 0000000000000000000000000000000000000000..22018b9d128592e65769217541880ac6f4b0923a --- /dev/null +++ b/PhysicsAnalysis/Algorithms/TriggerAnalysisAlgorithms/python/TriggerAnalysisSFConfig.py @@ -0,0 +1,158 @@ +# Copyright (C) 2002-2024 CERN for the benefit of the ATLAS collaboration + +# AnaAlgorithm import(s): +from AnalysisAlgorithmsConfig.ConfigBlock import ConfigBlock +from AnalysisAlgorithmsConfig.ConfigAccumulator import DataType +from AthenaConfiguration.Enums import LHCPeriod +from Campaigns.Utils import Campaign + + +class TriggerAnalysisSFBlock (ConfigBlock): + """the ConfigBlock for trigger analysis""" + + def __init__ (self, configName='') : + super (TriggerAnalysisSFBlock, self).__init__ () + self.addDependency('Electrons', required=False) + self.addDependency('Photons', required=False) + self.addDependency('Muons', required=False) + self.addDependency('OverlapRemoval', required=False) + + self.addOption ('triggerChainsPerYear', {}, type=None, + info="a dictionary with key (string) the year and value (list of " + "strings) the trigger chains. You can also use || within a string " + "to enforce an OR of triggers without looking up the individual " + "triggers. Used for both trigger selection and SFs. " + "The default is {} (empty dictionary).") + self.addOption ('noFilter', False, type=bool, + info="do not apply an event filter. The default is False, i.e. " + "remove events not passing trigger selection and matching.") + 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 ('photonIsol', '', type=str, + info="the photon isolation WP (string) to use.") + self.addOption ('muonID', '', type=str, + info="the muon quality WP (string) to use.") + self.addOption ('electrons', '', type=str, + info="the input electron container, with a possible selection, in " + "the format container or container.selection.") + self.addOption ('muons', '', type=str, + info="the input muon container, with a possible selection, in the " + "format container or container.selection.") + self.addOption ('photons', '', type=str, + info="the input photon container, with a possible selection, in " + "the format container or container.selection.") + 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. Still performs the global trigger " + "matching (same behaviour as on data). The default is False.") + self.addOption ('noGlobalTriggerEff', False, type=bool, + info="disables the global trigger efficiency tool (including " + "matching), which is only suited for electron/muon/photon " + "trigger legs. The default is False.") + + + def makeTriggerDecisionTool(self, config): + # Might have already been added in TriggerAnalysisBlock + if "TrigDecisionTool" in config._algorithms: + return config._algorithms["TrigDecisionTool"] + + # Create public trigger tools + xAODConfTool = config.createPublicTool( 'TrigConf::xAODConfigTool', 'xAODConfigTool' ) + decisionTool = config.createPublicTool( 'Trig::TrigDecisionTool', 'TrigDecisionTool' ) + decisionTool.ConfigTool = '%s/%s' % \ + ( xAODConfTool.getType(), xAODConfTool.getName() ) + if config.geometry() == LHCPeriod.Run3: + decisionTool.NavigationFormat = 'TrigComposite' # Read Run 3 navigation (options are "TrigComposite" for R3 or "TriggElement" for R2, R2 navigation is not kept in most DAODs) + decisionTool.HLTSummary = 'HLTNav_Summary_DAODSlimmed' # Name of R3 navigation container (if reading from AOD, then "HLTNav_Summary_AODSlimmed" instead) + + return decisionTool + + def makeTriggerMatchingTool(self, config, decisionTool): + + # Create public trigger tools + drScoringTool = config.createPublicTool( 'Trig::DRScoringTool', 'DRScoringTool' ) + if config.geometry() == LHCPeriod.Run3: + matchingTool = config.createPublicTool( 'Trig::R3MatchingTool', 'MatchingTool' ) + matchingTool.ScoringTool = '%s/%s' % \ + ( drScoringTool.getType(), drScoringTool.getName() ) + matchingTool.TrigDecisionTool = '%s/%s' % \ + ( decisionTool.getType(), decisionTool.getName() ) + else: + matchingTool = config.createPublicTool( 'Trig::MatchFromCompositeTool', 'MatchingTool' ) + if config.isPhyslite(): + matchingTool.InputPrefix = "AnalysisTrigMatch_" + + return matchingTool + + + def makeTriggerGlobalEffCorrAlg(self, config, matchingTool, noSF): + + alg = config.createAlgorithm( 'CP::TrigGlobalEfficiencyAlg', 'TrigGlobalSFAlg' ) + if config.geometry() == LHCPeriod.Run3: + alg.triggers_2022 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in self.triggerChainsPerYear.get('2022',[])] + alg.triggers_2023 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in self.triggerChainsPerYear.get('2023',[])] + alg.triggers_2024 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in self.triggerChainsPerYear.get('2024',[])] + alg.triggers_2025 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in self.triggerChainsPerYear.get('2025',[])] + if config.campaign() in [Campaign.MC21a, Campaign.MC23a]: + if not alg.triggers_2022: + raise ValueError( 'TriggerAnalysisConfig: you must provide a set of triggers for the year 2022!' ) + elif config.campaign() is Campaign.MC23c: + if not alg.triggers_2023: + raise ValueError( 'TriggerAnalysisConfig: you must provide a set of triggers for the year 2023!' ) + else: + alg.triggers_2015 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in self.triggerChainsPerYear.get('2015',[])] + alg.triggers_2016 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in self.triggerChainsPerYear.get('2016',[])] + alg.triggers_2017 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in self.triggerChainsPerYear.get('2017',[])] + alg.triggers_2018 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in self.triggerChainsPerYear.get('2018',[])] + if config.campaign() is Campaign.MC20a: + if not (alg.triggers_2015 and alg.triggers_2016): + raise ValueError( 'TriggerAnalysisConfig: you must provide a set of triggers for the years 2015 and 2016!' ) + elif config.campaign() is Campaign.MC20d: + if not alg.triggers_2017: + raise ValueError( 'TriggerAnalysisConfig: you must provide a set of triggers for the year 2017!' ) + elif config.campaign() is Campaign.MC20e: + if not alg.triggers_2018: + raise ValueError( 'TriggerAnalysisConfig: you must provide a set of triggers for the year 2018!' ) + + alg.matchingTool = '%s/%s' % ( matchingTool.getType(), matchingTool.getName() ) + alg.isRun3Geo = config.geometry() == LHCPeriod.Run3 + alg.scaleFactorDecoration = 'globalTriggerEffSF_%SYS%' + alg.matchingDecoration = 'globalTriggerMatch_%SYS%' + alg.eventDecisionOutputDecoration = 'dontsave_%SYS%' + alg.doMatchingOnly = config.dataType() is DataType.Data or noSF + alg.noFilter = self.noFilter + alg.electronID = self.electronID + alg.electronIsol = self.electronIsol + alg.photonIsol = self.photonIsol + alg.muonID = self.muonID + if self.electrons: + alg.electrons, alg.electronSelection = config.readNameAndSelection(self.electrons) + if self.muons: + alg.muons, alg.muonSelection = config.readNameAndSelection(self.muons) + if self.photons: + alg.photons, alg.photonSelection = config.readNameAndSelection(self.photons) + if not (self.electrons or self.muons or self.photons): + raise ValueError ('TriggerAnalysisConfig: at least one object collection must be provided! (electrons, muons, photons)' ) + + if config.dataType() != DataType.Data and not alg.doMatchingOnly: + config.addOutputVar ('EventInfo', alg.scaleFactorDecoration, 'globalTriggerEffSF') + config.addOutputVar ('EventInfo', alg.matchingDecoration, 'globalTriggerMatch', noSys=True) + + return + + def makeAlgs (self, config) : + + # Create the decision algorithm, keeping track of the decision tool for later + decisionTool = self.makeTriggerDecisionTool(config) + + # Now pass it to the matching algorithm, keeping track of the matching tool for later + matchingTool = self.makeTriggerMatchingTool(config, decisionTool) + + # Calculate multi-lepton (electron/muon/photon) trigger efficiencies and SFs + if self.triggerChainsPerYear and not self.noGlobalTriggerEff: + self.makeTriggerGlobalEffCorrAlg(config, matchingTool, self.noEffSF) + + return