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  self.addOption ('noGlobalTriggerEff', False, type=bool,
118  info="disables the global trigger efficiency tool (including "
119  "matching), which is only suited for electron/muon/photon "
120  "trigger legs. The default is False.")
121  self.addOption ('separateChainMatching', False, type=bool,
122  info="Store the matching status for each trigger separately")
123  self.addOption ('triggerMatchingChainsPerYear', {}, type=None,
124  info="a dictionary with key (string) the year and value (list of "
125  "strings) the trigger chains. The default is {} (empty dictionary).")
126  self.addOption("includeAllYearsPerRun", False, type=bool,
127  info="if True, trigger matching will include all configured years "
128  "in the LHC run in all jobs. The default is False.")
129  self.addOption ('postfix', '', type=str,
130  info="a unique identifier for the trigger matching decorations. Only "
131  "useful when defining multiple setups. The default is '' (empty string).")
132 
134  self,
135  config: ConfigAccumulator,
136  matchingTool,
137  noSF: bool,
138  triggerSuffix: str = ''
139  ) -> None:
140  alg = config.createAlgorithm( 'CP::TrigGlobalEfficiencyAlg', 'TrigGlobalSFAlg' + triggerSuffix + self.postfix)
141  if config.geometry() is LHCPeriod.Run3:
142  alg.triggers_2022 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2022)]
143  alg.triggers_2023 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2023)]
144  alg.triggers_2024 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2024)]
145  alg.triggers_2025 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2025)]
146  if is_mc_from(config, Campaign.MC23a) or is_data_from(config, 2022):
147  if not alg.triggers_2022:
148  raise ValueError('TriggerAnalysisConfig: you must provide a set of triggers for the year 2022!')
149  elif is_mc_from(config, Campaign.MC23d) or is_data_from(config, 2023):
150  if not alg.triggers_2023:
151  raise ValueError('TriggerAnalysisConfig: you must provide a set of triggers for the year 2023!')
152  else:
153  alg.triggers_2015 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2015)]
154  alg.triggers_2016 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2016)]
155  alg.triggers_2017 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2017)]
156  alg.triggers_2018 = [trig.replace("HLT_","").replace(" || ", "_OR_") for trig in get_year_data(self.triggerChainsPerYear, 2018)]
157  if is_mc_from(config, Campaign.MC20a):
158  if not (alg.triggers_2015 and alg.triggers_2016):
159  raise ValueError('TriggerAnalysisConfig: you must provide a set of triggers for the years 2015 and 2016!')
160  elif is_data_from(config, 2015):
161  if not alg.triggers_2015:
162  raise ValueError('TriggerAnalysisConfig: you must provide a set of triggers for the year 2015!')
163  elif is_data_from(config, 2016):
164  if not alg.triggers_2016:
165  raise ValueError('TriggerAnalysisConfig: you must provide a set of triggers for the year 2016!')
166  elif is_mc_from(config, Campaign.MC20d) or is_data_from(config, 2017):
167  if not alg.triggers_2017:
168  raise ValueError('TriggerAnalysisConfig: you must provide a set of triggers for the year 2017!')
169  elif is_mc_from(config, Campaign.MC20e) or is_data_from(config, 2018):
170  if not alg.triggers_2018:
171  raise ValueError('TriggerAnalysisConfig: you must provide a set of triggers for the year 2018!')
172 
173  triggerMatchingChains = set()
174  if self.separateChainMatching:
175  for year in get_input_years(config):
176  for trig in get_year_data(self.triggerChainsPerYear, year):
177  triggerMatchingChains.update(trig.replace(' || ', '_OR_').split('_OR_'))
178  alg.separateMatchingTriggers = list(triggerMatchingChains)
179  alg.separateMatchingDecorationSuffix = triggerSuffix + self.postfix
180 
181  alg.matchingTool = '%s/%s' % ( matchingTool.getType(), matchingTool.getName() )
182  alg.isRun3Geo = config.geometry() is LHCPeriod.Run3
183  alg.numberOfToys = self.numberOfToys
184  alg.scaleFactorDecoration = 'globalTriggerEffSF' + triggerSuffix + self.postfix + '_%SYS%'
185  alg.matchingDecoration = 'globalTriggerMatch' + triggerSuffix + self.postfix + '_%SYS%'
186  alg.eventDecisionOutputDecoration = 'globalTriggerMatch' + triggerSuffix + self.postfix + '_dontsave_%SYS%'
187  alg.doMatchingOnly = config.dataType() is DataType.Data or noSF
188  alg.noFilter = self.noFilter
189  alg.electronID = self.electronID
190  alg.electronIsol = self.electronIsol
191  alg.photonIsol = self.photonIsol
192  alg.muonID = self.muonID
193  if self.electrons:
194  alg.electrons, alg.electronSelection = config.readNameAndSelection(self.electrons)
195  if self.muons:
196  alg.muons, alg.muonSelection = config.readNameAndSelection(self.muons)
197  if self.photons:
198  alg.photons, alg.photonSelection = config.readNameAndSelection(self.photons)
199  if not (self.electrons or self.muons or self.photons):
200  raise ValueError('TriggerAnalysisConfig: at least one object collection must be provided! (electrons, muons, photons)' )
201 
202  if config.dataType() is not DataType.Data and not alg.doMatchingOnly:
203  config.addOutputVar('EventInfo', alg.scaleFactorDecoration, 'globalTriggerEffSF' + triggerSuffix + self.postfix)
204  config.addOutputVar('EventInfo', alg.matchingDecoration, 'globalTriggerMatch' + triggerSuffix + self.postfix, noSys=False)
205 
206  for trig in triggerMatchingChains:
207  var = f'triggerMatch_{trig.replace("-", "_").replace(".", "p")}{alg.separateMatchingDecorationSuffix}'
208  config.addOutputVar('EventInfo', f'{var}_%SYS%', var, noSys=False)
209 
210  return
211 
213  self,
214  config: ConfigAccumulator,
215  matchingTool,
216  particles,
217  trig_string: str = "",
218  trig_chains: set = None,
219  trig_chains_dummy: set = None,
220  ) -> None:
221  if not particles or (not trig_chains and not trig_chains_dummy):
222  return
223 
224  if not any(trig_string in trig for trig in trig_chains) \
225  and not any(trig_string in trig for trig in trig_chains_dummy):
226  return
227 
228  alg = config.createAlgorithm("CP::TrigMatchingAlg", f"TrigMatchingAlg_{trig_string}{self.postfix}")
229  alg.matchingTool = f"{matchingTool.getType()}/{matchingTool.getName()}"
230  alg.matchingDecoration = f"trigMatched{self.postfix}"
231  alg.trigSingleMatchingList = [trig for trig in trig_chains if trig_string in trig]
232  alg.trigSingleMatchingListDummy = [trig for trig in trig_chains_dummy if trig_string in trig]
233  alg.particles, alg.particleSelection = config.readNameAndSelection(particles)
234 
235  for trig in list(alg.trigSingleMatchingList) + list(alg.trigSingleMatchingListDummy):
236  trig = trig.replace(".", "p").replace("-", "_").replace(" ", "")
237  if trig_string in trig:
238  config.addOutputVar(particles.split(".")[0], f"trigMatched_{self.postfix}{trig}", f"trigMatched_{self.postfix}{trig}", noSys=True)
239 
241  self,
242  config: ConfigAccumulator,
243  matchingTool,
244  ) -> None:
245  years = get_input_years(config)
246 
247  triggerMatchingChains = set()
248  triggerMatchingChainsDummy = set()
249  for year in years:
250  for trig in get_year_data(self.triggerMatchingChainsPerYear, year):
251  trig = trig.replace(' || ', '_OR_')
252  triggerMatchingChains.update(trig.split('_OR_'))
253  if self.includeAllYearsPerRun:
254  triggerMatchingChainsAll = set()
255  for year in self.triggerMatchingChainsPerYear:
256  if not is_year_in_current_period(config, year):
257  continue
258  for trig in get_year_data(self.triggerMatchingChainsPerYear, year):
259  trig = trig.replace(' || ', '_OR_')
260  triggerMatchingChainsAll.update(trig.split('_OR_'))
261  triggerMatchingChainsDummy = triggerMatchingChainsAll - triggerMatchingChains
262 
263  self.createTrigMatching(config, matchingTool, self.electrons, 'HLT_e', triggerMatchingChains, triggerMatchingChainsDummy)
264  self.createTrigMatching(config, matchingTool, self.muons, 'HLT_mu', triggerMatchingChains, triggerMatchingChainsDummy)
265  self.createTrigMatching(config, matchingTool, self.photons, 'HLT_g', triggerMatchingChains, triggerMatchingChainsDummy)
266  self.createTrigMatching(config, matchingTool, self.taus, 'HLT_tau', triggerMatchingChains, triggerMatchingChainsDummy)
267 
268  return
269 
270  def makeAlgs(self, config: ConfigAccumulator) -> None:
271  if (
273  self.triggerChainsPerYear and
275  ):
276  raise Exception('multiTriggerChainsPerYear and triggerChainsPerYear cannot be configured at the same time!')
277 
278  if self.triggerChainsPerYear and not self.multiTriggerChainsPerYear:
280 
281  # Create the decision algorithm, keeping track of the decision tool for later
282  decisionTool = TriggerAnalysisBlock.makeTriggerDecisionTool(config)
283 
284  # Now pass it to the matching algorithm, keeping track of the matching tool for later
285  matchingTool = TriggerAnalysisBlock.makeTriggerMatchingTool(config, decisionTool)
286 
287  # Calculate multi-lepton (electron/muon/photon) trigger efficiencies and SFs
288  if self.multiTriggerChainsPerYear and not self.noGlobalTriggerEff:
289  for suffix, trigger_chains in self.multiTriggerChainsPerYear.items():
290  self.triggerChainsPerYear = trigger_chains
291  self.makeTriggerGlobalEffCorrAlg(config, matchingTool, self.noEffSF, suffix)
292 
293  # Save trigger matching information (currently only single leg trigger are supported)
294  if self.triggerMatchingChainsPerYear:
295  self.makeTrigMatchingAlg(config, matchingTool)
296 
297  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:279
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:133
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:270
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:290
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:212
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:240
str
Definition: BTagTrackIpAccessor.cxx:11
Trk::split
@ split
Definition: LayerMaterialProperties.h:38