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=dict,
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 self.addOption ('multiTriggerChainsPerYear', {}, type=dict,
93 info="a dictionary with key (string) a trigger set name and value a "
94 "`triggerChainsPerYear` dictionary, following the previous convention. "
95 "Relevant for analyses using different triggers in different categories, "
96 "where the trigger global scale factors shouldn't be combined.")
97 self.addOption ('noFilter', False, type=bool,
98 info="do not apply an event filter (i.e. keep events "
99 "not passing trigger selection and matching).")
100 self.addOption ('electronID', '', type=str,
101 info="the electron ID WP to use.")
102 self.addOption ('electronIsol', '', type=str,
103 info="the electron isolation WP to use.")
104 self.addOption ('photonIsol', '', type=str,
105 info="the photon isolation WP to use.")
106 self.addOption ('muonID', '', type=str,
107 info="the muon quality WP to use.")
108 self.addOption ('electrons', '', type=str,
109 info="the input electron container, with a possible selection, in "
110 "the format `container` or `container.selection`.")
111 self.addOption ('muons', '', type=str,
112 info="the input muon container, with a possible selection, in the "
113 "format `container` or `container.selection`.")
114 self.addOption ('photons', '', type=str,
115 info="the input photon container, with a possible selection, in "
116 "the format `container` or `container.selection`.")
117 self.addOption ('taus', '', type=str,
118 info="the input tau-jet container, with a possible selection, in "
119 "the format `container` or `container.selection`.")
120 self.addOption ('numberOfToys', 0, type=int,
121 info="the number of toy experiments to run to estimate the trigger efficiencies, "
122 "instead of using explicit formulas. Set it to 0 to not use toys.")
123 self.addOption ('noEffSF', False, type=bool,
124 info="disables the calculation of efficiencies and scale factors. "
125 "Experimental! only useful to test a new WP for which scale "
126 "factors are not available. Still performs the global trigger "
127 "matching (same behaviour as on data).",
128 expertMode=True)
129 self.addOption ('noGlobalTriggerEff', False, type=bool,
130 info="disables the global trigger efficiency tool (including "
131 "matching), which is only suited for electron/muon/photon "
132 "trigger legs.")
133 self.addOption ('separateChainMatching', False, type=bool,
134 info="store the matching status for each trigger separately.")
135 self.addOption ('triggerMatchingChainsPerYear', {}, type=dict,
136 info="a dictionary with key (string) the year and value (list of "
137 "strings) the trigger chains.")
138 self.addOption("includeAllYearsPerRun", False, type=bool,
139 info="trigger matching will include all configured years "
140 "in the LHC run in all jobs.")
141 self.addOption ('postfix', '', type=str,
142 info="a unique identifier for the trigger matching decorations. Only "
143 "useful when defining multiple setups.")
144
146 self,
147 config: ConfigAccumulator,
148 matchingTool,
149 noSF: bool,
150 triggerSuffix: str = ''
151 ) -> None:
152 alg = config.createAlgorithm( 'CP::TrigGlobalEfficiencyAlg', 'TrigGlobalSFAlg' + triggerSuffix + self.postfix)
153 if config.geometry() is LHCPeriod.Run3:
154 alg.triggers_2022 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2022)]
155 alg.triggers_2023 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2023)]
156 alg.triggers_2024 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2024)]
157 alg.triggers_2025 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2025)]
158 if is_mc_from(config, Campaign.MC23a) or is_data_from(config, 2022):
159 if not alg.triggers_2022:
160 raise ValueError('TriggerAnalysisConfig: you must provide a set of triggers for the year 2022!')
161 elif is_mc_from(config, Campaign.MC23d) or is_data_from(config, 2023):
162 if not alg.triggers_2023:
163 raise ValueError('TriggerAnalysisConfig: you must provide a set of triggers for the year 2023!')
164 else:
165 alg.triggers_2015 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2015)]
166 alg.triggers_2016 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2016)]
167 alg.triggers_2017 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2017)]
168 alg.triggers_2018 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2018)]
169 if is_mc_from(config, Campaign.MC20a):
170 if not (alg.triggers_2015 and alg.triggers_2016):
171 raise ValueError('TriggerAnalysisConfig: you must provide a set of triggers for the years 2015 and 2016!')
172 elif is_data_from(config, 2015):
173 if not alg.triggers_2015:
174 raise ValueError('TriggerAnalysisConfig: you must provide a set of triggers for the year 2015!')
175 elif is_data_from(config, 2016):
176 if not alg.triggers_2016:
177 raise ValueError('TriggerAnalysisConfig: you must provide a set of triggers for the year 2016!')
178 elif is_mc_from(config, Campaign.MC20d) or is_data_from(config, 2017):
179 if not alg.triggers_2017:
180 raise ValueError('TriggerAnalysisConfig: you must provide a set of triggers for the year 2017!')
181 elif is_mc_from(config, Campaign.MC20e) or is_data_from(config, 2018):
182 if not alg.triggers_2018:
183 raise ValueError('TriggerAnalysisConfig: you must provide a set of triggers for the year 2018!')
184
185 triggerMatchingChains = set()
187 for year in get_input_years(config):
188 for trig in get_year_data(self.triggerChainsPerYear, year):
189 triggerMatchingChains.update(trig.replace(' || ', '_OR_').split('_OR_'))
190 alg.separateMatchingTriggers = list(triggerMatchingChains)
191 alg.separateMatchingDecorationSuffix = triggerSuffix + self.postfix
192
193 alg.matchingTool = '%s/%s' % ( matchingTool.getType(), matchingTool.getName() )
194 alg.isRun3Geo = config.geometry() is LHCPeriod.Run3
195 alg.campaign = config.campaign().value
196 alg.numberOfToys = self.numberOfToys
197 alg.scaleFactorDecoration = 'globalTriggerEffSF' + triggerSuffix + self.postfix + '_%SYS%'
198 alg.matchingDecoration = 'globalTriggerMatch' + triggerSuffix + self.postfix + '_%SYS%'
199 alg.eventDecisionOutputDecoration = 'globalTriggerMatch' + triggerSuffix + self.postfix + '_dontsave_%SYS%'
200 alg.doMatchingOnly = config.dataType() is DataType.Data or noSF
201 alg.noFilter = self.noFilter
202 alg.electronID = self.electronID
203 alg.electronIsol = self.electronIsol
204 alg.photonIsol = self.photonIsol
205 alg.muonID = self.muonID
206 if self.electrons:
207 alg.electrons, alg.electronSelection = config.readNameAndSelection(self.electrons)
208 if self.muons:
209 alg.muons, alg.muonSelection = config.readNameAndSelection(self.muons)
210 if self.photons:
211 alg.photons, alg.photonSelection = config.readNameAndSelection(self.photons)
212 if not (self.electrons or self.muons or self.photons):
213 raise ValueError('TriggerAnalysisConfig: at least one object collection must be provided! (electrons, muons, photons)' )
214
215 if config.dataType() is not DataType.Data and not alg.doMatchingOnly:
216 config.addOutputVar('EventInfo', alg.scaleFactorDecoration, 'globalTriggerEffSF' + triggerSuffix + self.postfix)
217 config.addOutputVar('EventInfo', alg.matchingDecoration, 'globalTriggerMatch' + triggerSuffix + self.postfix, noSys=False)
218
219 for trig in triggerMatchingChains:
220 var = f'triggerMatch_{trig.replace("-", "_").replace(".", "p")}{alg.separateMatchingDecorationSuffix}'
221 config.addOutputVar('EventInfo', f'{var}_%SYS%', var, noSys=False)
222
223 return
224
226 self,
227 config: ConfigAccumulator,
228 matchingTool,
229 particles,
230 trig_string: str = "",
231 trig_chains: set = None,
232 trig_chains_dummy: set = None,
233 ) -> None:
234 if not particles or (not trig_chains and not trig_chains_dummy):
235 return
236
237 if not any(trig_string in trig for trig in trig_chains) \
238 and not any(trig_string in trig for trig in trig_chains_dummy):
239 return
240
241 alg = config.createAlgorithm("CP::TrigMatchingAlg", f"TrigMatchingAlg_{trig_string}{self.postfix}")
242 alg.matchingTool = f"{matchingTool.getType()}/{matchingTool.getName()}"
243 alg.matchingDecoration = f"trigMatched{self.postfix}"
244 alg.trigSingleMatchingList = [trig for trig in trig_chains if trig_string in trig]
245 alg.trigSingleMatchingListDummy = [trig for trig in trig_chains_dummy if trig_string in trig]
246 alg.particles, alg.particleSelection = config.readNameAndSelection(particles)
247
248 for trig in list(alg.trigSingleMatchingList) + list(alg.trigSingleMatchingListDummy):
249 trig = trig.replace(".", "p").replace("-", "_").replace(" ", "")
250 if trig_string in trig:
251 config.addOutputVar(particles.split(".")[0], f"trigMatched_{self.postfix}{trig}", f"trigMatched_{self.postfix}{trig}", noSys=True)
252
254 self,
255 config: ConfigAccumulator,
256 matchingTool,
257 ) -> None:
258 years = get_input_years(config)
259
260 triggerMatchingChains = set()
261 triggerMatchingChainsDummy = set()
262 for year in years:
264 trig = trig.replace(' || ', '_OR_')
265 triggerMatchingChains.update(trig.split('_OR_'))
267 triggerMatchingChainsAll = set()
268 for year in self.triggerMatchingChainsPerYear:
269 if not is_year_in_current_period(config, year):
270 continue
271 for trig in get_year_data(self.triggerMatchingChainsPerYear, year):
272 trig = trig.replace(' || ', '_OR_')
273 triggerMatchingChainsAll.update(trig.split('_OR_'))
274 triggerMatchingChainsDummy = triggerMatchingChainsAll - triggerMatchingChains
275
276 self.createTrigMatching(config, matchingTool, self.electrons, 'HLT_e', triggerMatchingChains, triggerMatchingChainsDummy)
277 self.createTrigMatching(config, matchingTool, self.muons, 'HLT_mu', triggerMatchingChains, triggerMatchingChainsDummy)
278 self.createTrigMatching(config, matchingTool, self.photons, 'HLT_g', triggerMatchingChains, triggerMatchingChainsDummy)
279 self.createTrigMatching(config, matchingTool, self.taus, 'HLT_tau', triggerMatchingChains, triggerMatchingChainsDummy)
280
281 return
282
283 def makeAlgs(self, config: ConfigAccumulator) -> None:
284 if (
286 self.triggerChainsPerYear and
287 self.triggerChainsPerYear not in self.multiTriggerChainsPerYear.values()
288 ):
289 raise Exception('multiTriggerChainsPerYear and triggerChainsPerYear cannot be configured at the same time!')
290
293
294 # Create the decision algorithm, keeping track of the decision tool for later
295 decisionTool = TriggerAnalysisBlock.makeTriggerDecisionTool(config)
296
297 # Now pass it to the matching algorithm, keeping track of the matching tool for later
298 matchingTool = TriggerAnalysisBlock.makeTriggerMatchingTool(config, decisionTool)
299
300 # Calculate multi-lepton (electron/muon/photon) trigger efficiencies and SFs
302 for suffix, trigger_chains in self.multiTriggerChainsPerYear.items():
303 self.triggerChainsPerYear = trigger_chains
304 self.makeTriggerGlobalEffCorrAlg(config, matchingTool, self.noEffSF, suffix)
305
306 # Save trigger matching information (currently only single leg trigger are supported)
308 self.makeTrigMatchingAlg(config, matchingTool)
309
310 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)