ATLAS Offline Software
Loading...
Searching...
No Matches
TriggerAnalysisSFConfig.py
Go to the documentation of this file.
1# Copyright (C) 2002-2024 CERN for the benefit of the ATLAS collaboration
2from typing import Iterable, Union
3
4from AnalysisAlgorithmsConfig.ConfigBlock import ConfigBlock
5from AnalysisAlgorithmsConfig.ConfigAccumulator import DataType, ConfigAccumulator
6from AthenaConfiguration.Enums import LHCPeriod
7from Campaigns.Utils import Campaign
8from TriggerAnalysisAlgorithms.TriggerAnalysisConfig import TriggerAnalysisBlock, is_year_in_current_period
9
10
11def is_mc_from(config: ConfigAccumulator, campaign_list: Union[Campaign, Iterable[Campaign]]) -> bool:
12 """
13 Utility function to check whether the data type is mc and whether the campaign is in desired list of campaigns
14 without causing an invalid FlugEnum comparison
15 """
16 campaign_list = [campaign_list] if isinstance(campaign_list, Campaign) else campaign_list
17 return config.dataType() is not DataType.Data and config.campaign() in campaign_list
18
19
20def is_data_from(config, data_year_list: Union[int, Iterable[int]]) -> bool:
21 """
22 Utility function to check whether the data type is data and whether the year is in desired list of years
23 """
24 data_year_list = [data_year_list] if isinstance(data_year_list, int) else data_year_list
25 return config.dataType() is DataType.Data and config.dataYear() in data_year_list
26
27
28def get_year_data(dictionary: dict, year: int | str) -> list:
29 """
30 Utility function to get the data for a specific year from a dictionary
31 """
32 return dictionary.get(int(year), dictionary.get(str(year), []))
33
34
35def get_input_years(config: ConfigAccumulator) -> list[int]:
36 """
37 Utility function to get the list of years that the input corresponds to
38 """
39 years = []
40 if config.campaign() is Campaign.MC20a:
41 years = [2015, 2016]
42 elif is_data_from(config, 2015):
43 years = [2015]
44 elif is_data_from(config, 2016):
45 years = [2016]
46 elif config.campaign() is Campaign.MC20d or is_data_from(config, 2017):
47 years = [2017]
48 elif config.campaign() is Campaign.MC20e or is_data_from(config, 2018):
49 years = [2018]
50 elif config.campaign() is Campaign.MC23a or is_data_from(config, 2022):
51 years = [2022]
52 elif config.campaign() is Campaign.MC23d or is_data_from(config, 2023):
53 years = [2023]
54 elif config.campaign() is Campaign.MC23e or is_data_from(config, 2024):
55 years = [2024]
56 elif config.campaign() is Campaign.MC23g or is_data_from(config, 2025):
57 years = [2025]
58 else:
59 raise ValueError('TriggerAnalysisConfig: unable to deduce data taking year for input file')
60
61 return years
62
63
64def trigger_set(config, triggerChainsPerYear, includeAllYearsPerRun):
65 triggers = set()
66 if includeAllYearsPerRun:
67 for year in triggerChainsPerYear:
68 if not is_year_in_current_period(config, year):
69 continue
70 triggers.update(get_year_data(triggerChainsPerYear, year))
71 else:
72 for year in get_input_years(config):
73 triggers.update(get_year_data(triggerChainsPerYear, year))
74 return triggers
75
76
77class TriggerAnalysisSFBlock(ConfigBlock):
78 """the ConfigBlock for trigger analysis"""
79 def __init__(self):
80 super(TriggerAnalysisSFBlock, self).__init__()
81 self.addDependency('Electrons', required=False)
82 self.addDependency('Photons', required=False)
83 self.addDependency('Muons', required=False)
84 self.addDependency('Taus', required=False)
85 self.addDependency('OverlapRemoval', required=False)
86
87 self.addOption ('triggerChainsPerYear', {}, type=None,
88 info="a dictionary with key (string) the year and value (list of "
89 "strings) the trigger chains. You can also use || within a string "
90 "to enforce an OR of triggers without looking up the individual "
91 "triggers. Used for both trigger selection and SFs. "
92 "The default is {} (empty dictionary).")
93 self.addOption ('multiTriggerChainsPerYear', {}, type=None,
94 info="a dictionary with key (string) a trigger set name and value a "
95 "triggerChainsPerYear dictionary, following the previous convention. "
96 "Relevant for analyses using different triggers in different categories, "
97 "where the trigger global scale factors shouldn't be combined. "
98 "The default is {} (empty dictionary).")
99 self.addOption ('noFilter', False, type=bool,
100 info="do not apply an event filter. The default is False, i.e. "
101 "remove events not passing trigger selection and matching.")
102 self.addOption ('electronID', '', type=str,
103 info="the electron ID WP (string) to use.")
104 self.addOption ('electronIsol', '', type=str,
105 info="the electron isolation WP (string) to use.")
106 self.addOption ('photonIsol', '', type=str,
107 info="the photon isolation WP (string) to use.")
108 self.addOption ('muonID', '', type=str,
109 info="the muon quality WP (string) to use.")
110 self.addOption ('electrons', '', type=str,
111 info="the input electron container, with a possible selection, in "
112 "the format container or container.selection.")
113 self.addOption ('muons', '', type=str,
114 info="the input muon container, with a possible selection, in the "
115 "format container or container.selection.")
116 self.addOption ('photons', '', type=str,
117 info="the input photon container, with a possible selection, in "
118 "the format container or container.selection.")
119 self.addOption ('taus', '', type=str,
120 info="the input tau container, with a possible selection, in "
121 "the format container or container.selection.")
122 self.addOption ('numberOfToys', 0, type=int,
123 info="Number of toy experiments to run to estimate the trigger efficiencies, "
124 "instead of using explicit formulas. The default is 0 (not using toys).")
125 self.addOption ('noEffSF', False, type=bool,
126 info="disables the calculation of efficiencies and scale factors. "
127 "Experimental! only useful to test a new WP for which scale "
128 "factors are not available. Still performs the global trigger "
129 "matching (same behaviour as on data). The default is False.",
130 expertMode=True)
131 self.addOption ('noGlobalTriggerEff', False, type=bool,
132 info="disables the global trigger efficiency tool (including "
133 "matching), which is only suited for electron/muon/photon "
134 "trigger legs. The default is False.")
135 self.addOption ('separateChainMatching', False, type=bool,
136 info="Store the matching status for each trigger separately")
137 self.addOption ('triggerMatchingChainsPerYear', {}, type=None,
138 info="a dictionary with key (string) the year and value (list of "
139 "strings) the trigger chains. The default is {} (empty dictionary).")
140 self.addOption("includeAllYearsPerRun", False, type=bool,
141 info="if True, trigger matching will include all configured years "
142 "in the LHC run in all jobs. The default is False.")
143 self.addOption ('postfix', '', type=str,
144 info="a unique identifier for the trigger matching decorations. Only "
145 "useful when defining multiple setups. The default is '' (empty string).")
146
148 self,
149 config: ConfigAccumulator,
150 matchingTool,
151 noSF: bool,
152 triggerSuffix: str = ''
153 ) -> None:
154 alg = config.createAlgorithm( 'CP::TrigGlobalEfficiencyAlg', 'TrigGlobalSFAlg' + triggerSuffix + self.postfix)
155 if config.geometry() is LHCPeriod.Run3:
156 alg.triggers_2022 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2022)]
157 alg.triggers_2023 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2023)]
158 alg.triggers_2024 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2024)]
159 alg.triggers_2025 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2025)]
160 if is_mc_from(config, Campaign.MC23a) or is_data_from(config, 2022):
161 if not alg.triggers_2022:
162 raise ValueError('TriggerAnalysisConfig: you must provide a set of triggers for the year 2022!')
163 elif is_mc_from(config, Campaign.MC23d) or is_data_from(config, 2023):
164 if not alg.triggers_2023:
165 raise ValueError('TriggerAnalysisConfig: you must provide a set of triggers for the year 2023!')
166 else:
167 alg.triggers_2015 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2015)]
168 alg.triggers_2016 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2016)]
169 alg.triggers_2017 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2017)]
170 alg.triggers_2018 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2018)]
171 if is_mc_from(config, Campaign.MC20a):
172 if not (alg.triggers_2015 and alg.triggers_2016):
173 raise ValueError('TriggerAnalysisConfig: you must provide a set of triggers for the years 2015 and 2016!')
174 elif is_data_from(config, 2015):
175 if not alg.triggers_2015:
176 raise ValueError('TriggerAnalysisConfig: you must provide a set of triggers for the year 2015!')
177 elif is_data_from(config, 2016):
178 if not alg.triggers_2016:
179 raise ValueError('TriggerAnalysisConfig: you must provide a set of triggers for the year 2016!')
180 elif is_mc_from(config, Campaign.MC20d) or is_data_from(config, 2017):
181 if not alg.triggers_2017:
182 raise ValueError('TriggerAnalysisConfig: you must provide a set of triggers for the year 2017!')
183 elif is_mc_from(config, Campaign.MC20e) or is_data_from(config, 2018):
184 if not alg.triggers_2018:
185 raise ValueError('TriggerAnalysisConfig: you must provide a set of triggers for the year 2018!')
186
187 triggerMatchingChains = set()
189 for year in get_input_years(config):
190 for trig in get_year_data(self.triggerChainsPerYear, year):
191 triggerMatchingChains.update(trig.replace(' || ', '_OR_').split('_OR_'))
192 alg.separateMatchingTriggers = list(triggerMatchingChains)
193 alg.separateMatchingDecorationSuffix = triggerSuffix + self.postfix
194
195 alg.matchingTool = '%s/%s' % ( matchingTool.getType(), matchingTool.getName() )
196 alg.isRun3Geo = config.geometry() is LHCPeriod.Run3
197 alg.numberOfToys = self.numberOfToys
198 alg.scaleFactorDecoration = 'globalTriggerEffSF' + triggerSuffix + self.postfix + '_%SYS%'
199 alg.matchingDecoration = 'globalTriggerMatch' + triggerSuffix + self.postfix + '_%SYS%'
200 alg.eventDecisionOutputDecoration = 'globalTriggerMatch' + triggerSuffix + self.postfix + '_dontsave_%SYS%'
201 alg.doMatchingOnly = config.dataType() is DataType.Data or noSF
202 alg.noFilter = self.noFilter
203 alg.electronID = self.electronID
204 alg.electronIsol = self.electronIsol
205 alg.photonIsol = self.photonIsol
206 alg.muonID = self.muonID
207 if self.electrons:
208 alg.electrons, alg.electronSelection = config.readNameAndSelection(self.electrons)
209 if self.muons:
210 alg.muons, alg.muonSelection = config.readNameAndSelection(self.muons)
211 if self.photons:
212 alg.photons, alg.photonSelection = config.readNameAndSelection(self.photons)
213 if not (self.electrons or self.muons or self.photons):
214 raise ValueError('TriggerAnalysisConfig: at least one object collection must be provided! (electrons, muons, photons)' )
215
216 if config.dataType() is not DataType.Data and not alg.doMatchingOnly:
217 config.addOutputVar('EventInfo', alg.scaleFactorDecoration, 'globalTriggerEffSF' + triggerSuffix + self.postfix)
218 config.addOutputVar('EventInfo', alg.matchingDecoration, 'globalTriggerMatch' + triggerSuffix + self.postfix, noSys=False)
219
220 for trig in triggerMatchingChains:
221 var = f'triggerMatch_{trig.replace("-", "_").replace(".", "p")}{alg.separateMatchingDecorationSuffix}'
222 config.addOutputVar('EventInfo', f'{var}_%SYS%', var, noSys=False)
223
224 return
225
227 self,
228 config: ConfigAccumulator,
229 matchingTool,
230 particles,
231 trig_string: str = "",
232 trig_chains: set = None,
233 trig_chains_dummy: set = None,
234 ) -> None:
235 if not particles or (not trig_chains and not trig_chains_dummy):
236 return
237
238 if not any(trig_string in trig for trig in trig_chains) \
239 and not any(trig_string in trig for trig in trig_chains_dummy):
240 return
241
242 alg = config.createAlgorithm("CP::TrigMatchingAlg", f"TrigMatchingAlg_{trig_string}{self.postfix}")
243 alg.matchingTool = f"{matchingTool.getType()}/{matchingTool.getName()}"
244 alg.matchingDecoration = f"trigMatched{self.postfix}"
245 alg.trigSingleMatchingList = [trig for trig in trig_chains if trig_string in trig]
246 alg.trigSingleMatchingListDummy = [trig for trig in trig_chains_dummy if trig_string in trig]
247 alg.particles, alg.particleSelection = config.readNameAndSelection(particles)
248
249 for trig in list(alg.trigSingleMatchingList) + list(alg.trigSingleMatchingListDummy):
250 trig = trig.replace(".", "p").replace("-", "_").replace(" ", "")
251 if trig_string in trig:
252 config.addOutputVar(particles.split(".")[0], f"trigMatched_{self.postfix}{trig}", f"trigMatched_{self.postfix}{trig}", noSys=True)
253
255 self,
256 config: ConfigAccumulator,
257 matchingTool,
258 ) -> None:
259 years = get_input_years(config)
260
261 triggerMatchingChains = set()
262 triggerMatchingChainsDummy = set()
263 for year in years:
265 trig = trig.replace(' || ', '_OR_')
266 triggerMatchingChains.update(trig.split('_OR_'))
268 triggerMatchingChainsAll = set()
269 for year in self.triggerMatchingChainsPerYear:
270 if not is_year_in_current_period(config, year):
271 continue
272 for trig in get_year_data(self.triggerMatchingChainsPerYear, year):
273 trig = trig.replace(' || ', '_OR_')
274 triggerMatchingChainsAll.update(trig.split('_OR_'))
275 triggerMatchingChainsDummy = triggerMatchingChainsAll - triggerMatchingChains
276
277 self.createTrigMatching(config, matchingTool, self.electrons, 'HLT_e', triggerMatchingChains, triggerMatchingChainsDummy)
278 self.createTrigMatching(config, matchingTool, self.muons, 'HLT_mu', triggerMatchingChains, triggerMatchingChainsDummy)
279 self.createTrigMatching(config, matchingTool, self.photons, 'HLT_g', triggerMatchingChains, triggerMatchingChainsDummy)
280 self.createTrigMatching(config, matchingTool, self.taus, 'HLT_tau', triggerMatchingChains, triggerMatchingChainsDummy)
281
282 return
283
284 def makeAlgs(self, config: ConfigAccumulator) -> None:
285 if (
287 self.triggerChainsPerYear and
288 self.triggerChainsPerYear not in self.multiTriggerChainsPerYear.values()
289 ):
290 raise Exception('multiTriggerChainsPerYear and triggerChainsPerYear cannot be configured at the same time!')
291
294
295 # Create the decision algorithm, keeping track of the decision tool for later
296 decisionTool = TriggerAnalysisBlock.makeTriggerDecisionTool(config)
297
298 # Now pass it to the matching algorithm, keeping track of the matching tool for later
299 matchingTool = TriggerAnalysisBlock.makeTriggerMatchingTool(config, decisionTool)
300
301 # Calculate multi-lepton (electron/muon/photon) trigger efficiencies and SFs
303 for suffix, trigger_chains in self.multiTriggerChainsPerYear.items():
304 self.triggerChainsPerYear = trigger_chains
305 self.makeTriggerGlobalEffCorrAlg(config, matchingTool, self.noEffSF, suffix)
306
307 # Save trigger matching information (currently only single leg trigger are supported)
309 self.makeTrigMatchingAlg(config, matchingTool)
310
311 return
None makeTrigMatchingAlg(self, ConfigAccumulator config, matchingTool)
None createTrigMatching(self, ConfigAccumulator config, matchingTool, particles, str trig_string="", set trig_chains=None, set trig_chains_dummy=None)
None makeTriggerGlobalEffCorrAlg(self, ConfigAccumulator config, matchingTool, bool noSF, str triggerSuffix='')
STL class.
std::string replace(std::string s, const std::string &s2, const std::string &s3)
Definition hcg.cxx:310
std::vector< std::string > split(const std::string &s, const std::string &t=":")
Definition hcg.cxx:177
list[int] get_input_years(ConfigAccumulator config)
list get_year_data(dict dictionary, int|str year)
bool is_data_from(config, Union[int, Iterable[int]] data_year_list)
trigger_set(config, triggerChainsPerYear, includeAllYearsPerRun)
bool is_mc_from(ConfigAccumulator config, Union[Campaign, Iterable[Campaign]] campaign_list)