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