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