ATLAS Offline Software
Loading...
Searching...
No Matches
OutputAnalysisConfig.py
Go to the documentation of this file.
1# Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
2
3# AnaAlgorithm import(s):
4from AnalysisAlgorithmsConfig.ConfigBlock import ConfigBlock
5from AnalysisAlgorithmsConfig.ConfigAccumulator import DataType
6from AnalysisAlgorithmsConfig.ConfigBlock import filter_dsids
7from AthenaCommon.Logging import logging
8import copy, re
9
10class OutputAnalysisConfig (ConfigBlock):
11 """the ConfigBlock for the MET configuration"""
12
13 def __init__ (self) :
14 super (OutputAnalysisConfig, self).__init__ ()
15 self.addOption ('postfix', '', type=str,
16 info="a postfix to apply to decorations and algorithm names. "
17 "Typically not needed here.")
18 self.addOption ('vars', [], type=list,
19 info="a list of mappings (list of strings) between containers and "
20 "decorations to output branches.")
21 self.addOption ('varsOnlyForMC', [], type=list,
22 info="same as `vars`, but for MC-only variables so as to avoid a "
23 "crash when running on data.")
24 self.addOption ('metVars', [], type=list,
25 info="a list of mappings (list of strings) between containers "
26 "and decorations to output branches. Specficially for MET "
27 "variables, where only the final MET term is retained.")
28 self.addOption ('truthMetVars', [], type=list,
29 info="a list of mappings (list of strings) between containers "
30 "and decorations to output branches for truth MET.")
31 self.addOption ('containers', {}, type=dict,
32 info="a dictionary mapping prefixes (key) to container names "
33 "(values) to be used when saving to the output tree. Branches "
34 "are then of the form `prefix_decoration`.")
35 self.addOption ('containersFullMET', {}, type=dict,
36 info="same as `containers`, but for MET containers that should be "
37 "saved with all terms (as opposed to just the final term). This "
38 "is useful for special studies. A container can appear both here and "
39 "in containers (with different prefixes).")
40 self.addOption ('containersOnlyForMC', {}, type=dict,
41 info="same as `containers`, but for MC-only containers so as to avoid "
42 "a crash when running on data.")
43 self.addOption ('containersOnlyForDSIDs', {}, type=dict,
44 info="specify which DSIDs are allowed to produce a given container. "
45 "This works like `onlyForDSIDs`: pass a list of DSIDs or regexps.")
46 self.addOption ('nonContainers', [], type=list,
47 info="a list of container names that are not actual containers but should be treated as non-containers.")
48 self.addOption ('treeName', 'analysis', type=str,
49 info="name of the output TTree (or RNTuple) to save.")
50 self.addOption ('streamName', 'ANALYSIS', type=str,
51 info="name of the output stream to save the tree in.")
52 self.addOption ('metTermName', 'Final', type=str,
53 info="the name of the MET term to save, turning the MET "
54 "container into a single object.")
55 self.addOption ('truthMetTermName', 'NonInt', type=str,
56 info="the name of the truth MET term to save, turning the MET "
57 "container into a single object.")
58 self.addOption ('storeSelectionFlags', True, type=bool,
59 info="whether to store one branch for each object selection.")
60 self.addOption ('selectionFlagPrefix', 'select', type=str,
61 info="the prefix used when naming selection branches.")
62 self.addOption ('commands', [], type=list,
63 info="a list of strings containing commands (regexp strings "
64 "prefaced by the keywords `enable` or `disable`) to turn on/off the "
65 "writing of branches to the output ntuple. If left empty, do not modify "
66 "the scheduled output branches.")
67 self.addOption ('commandsOnlyForDSIDs', {}, type=dict,
68 info="a dictionary with individual DSIDs as keys, and a list of strings "
69 "like for the `commands` option as items. These `commands` will only be run "
70 "for the corresponding DSID.")
71 self.addOption ('alwaysAddNosys', False, type=bool,
72 info="If set to `True`, all branches will be given a systematics suffix, "
73 "even if they have no systematics (beyond the nominal).")
74 self.addOption ('skipRedundantSelectionFlags', True, type=bool,
75 info="remove the redundant 'outputSelect' branches created by the Thinning step. "
76 "These could however be used to simplify downstream workflows, as in Easyjet. "
77 "The default is True.")
78 self.addOption ('outputFormat', 'TTree', type=str,
79 info="The output format, `TTree` or `RNTuple`.")
80 self.addOption ('defaultBasketSize', None, type=int,
81 info="default basket size for all branches in the output tree. "
82 "If not set (the default), no basket size is configured and ROOT's "
83 "default will be used.")
84
85 def instanceName (self) :
86 """Return the instance name for this block"""
87 if self.postfix is not None and self.postfix != '':
88 return self.postfix
89 return self.treeName
90
91 @staticmethod
92 def branchSortOrder (rule):
93 return rule.split('->')[1].strip()
94
95 def createOutputAlgs (self, config, name, vars):
96 """A helper function to create output algorithm"""
97 alg = config.createAlgorithm('CP::AsgxAODNTupleMakerAlg', name)
98 alg.TreeName = self.treeName
99 alg.RootStreamName = self.streamName
100 alg.NonContainers = list(self.nonContainers)
101 branchList = list(vars)
102 branchList.sort(key=self.branchSortOrder)
103 branchList_nosys = [branch for branch in branchList if "%SYS%" not in branch]
104 branchList_sys = [branch for branch in branchList if "%SYS%" in branch]
105 alg.Branches = branchList_nosys + branchList_sys
106 if self.defaultBasketSize is not None:
107 alg.DefaultBasketSize = self.defaultBasketSize
108 return alg
109
110 def makeAlgs (self, config) :
111
112 log = logging.getLogger('OutputAnalysisConfig')
113
114 self.containers = dict(self.containers)
115 self.vars = set(self.vars)
117 self.metVars = set(self.metVars)
119
120 # check for overlaps between containers and containersFullMET
121 overlapping_keys = set(self.containers.keys()).intersection(self.containersFullMET.keys())
122 if overlapping_keys:
123 # convert the set of overlapping keys to a list of strings for the message (represents the empty string too!)
124 keys_message = [repr(key) for key in overlapping_keys]
125 raise KeyError(f"containersFullMET would overwrite the following container keys: {', '.join(keys_message)}")
126 # move items in self.containersFullMET to containers
128
129 # merge the MC-specific branches and containers into the main list/dictionary only if we are not running on data
130 if config.dataType() is not DataType.Data:
131 self.vars |= self.varsOnlyForMC
132
133 # protect 'containers' against being overwritten
134 # find overlapping keys
135 overlapping_keys = set(self.containers.keys()).intersection(self.containersOnlyForMC.keys())
136 if overlapping_keys:
137 # convert the set of overlapping keys to a list of strings for the message (represents the empty string too!)
138 keys_message = [repr(key) for key in overlapping_keys]
139 raise KeyError(f"containersOnlyForMC would overwrite the following container keys: {', '.join(keys_message)}")
140
141 # move items in self.containersOnlyForMC to self.containers
143
144 # now filter the containers depending on DSIDs
146 for container, dsid_filters in self.containersOnlyForDSIDs.items():
147 if container not in self.containers:
148 log.warning("Skipping unrecognised container prefix '%s' for DSID-filtering in OutputAnalysisConfig...", container)
149 continue
150 if not filter_dsids (dsid_filters, config):
151 # if current DSID is not allowed for this container, remove it
152 log.info("Skipping container prefix '%s' due to DSID filtering...", container)
153 # filter branches for validated containers
154 for var in set(self.vars): # make a copy of the list to avoid modifying it while iterating
155 var_container = var.split('.')[0].replace('_NOSYS', '').replace('_%SYS%', '')
156 if var_container == self.containers[container]:
157 self.vars.remove(var)
158 log.info("Skipping branch definition '%s' for excluded container %s...", var, var_container)
159 # filter branches for MET variables
160 for var in set(self.metVars): # make a copy of the list to avoid modifying it while iterating
161 var_container = var.split('.')[0].replace('_NOSYS', '').replace('_%SYS%', '')
162 if var_container == self.containers[container]:
163 self.metVars.remove(var)
164 log.info("Skipping MET branch definition '%s' for excluded container %s...", var, var_container)
165 # filter branches for truth MET variables
166 for var in set(self.truthMetVars): # make a copy of the list to avoid modifying it while iterating
167 var_container = var.split('.')[0].replace('_NOSYS', '').replace('_%SYS%', '')
168 if var_container == self.containers[container]:
169 self.truthMetVars.remove(var)
170 log.info("Skipping truth MET branch definition '%s' for excluded container %s...", var, var_container)
171 # remove the container from the list at the end
172 self.containers.pop (container)
173
174 for prefix, container in self.containers.items():
175 origName = config.getOutputContainerOrigin(container)
176 if config.getContainerMeta(origName, "nonContainer", False):
177 self.nonContainers.append(origName)
178
180 self.createSelectionFlagBranches(config)
181
182 outputConfigs = {}
183 for prefix in self.containers.keys() :
184 containerName = self.containers[prefix]
185 outputDict = config.getOutputVars (containerName)
186 for outputName in outputDict :
187 outputConfig = copy.deepcopy (outputDict[outputName])
188 outputConfig.outputContainerName = config.readName(containerName)
189 outputConfig.prefix = prefix
190 # if the container is a MET container with all terms, we
191 # also need to write out the name of each MET term
192 if prefix in self.containersFullMET and outputConfig.variableName == 'name':
193 outputConfig.enabled = True
194 outputConfigs[prefix + outputName] = outputConfig
195
196 # check for DSID-specific commands
197 for dsid, dsid_commands in self.commandsOnlyForDSIDs.items():
198 if filter_dsids([dsid], config):
199 self.commands += dsid_commands
200
201 outputConfigsRename = {}
202 for command in self.commands :
203 words = command.split (' ')
204 if len (words) == 0 :
205 raise ValueError ('received empty command for "commands" option')
206 optional = words[0] == 'optional'
207 if optional :
208 words = words[1:] # remove the 'optional' keyword
209 if words[0] == 'enable' :
210 if len (words) != 2 :
211 raise ValueError ('enable takes exactly one argument: ' + command)
212 used = False
213 for name in outputConfigs :
214 if re.match (words[1], name) :
215 outputConfigs[name].enabled = True
216 used = True
217 if not used and not optional and config.dataType() is not DataType.Data:
218 raise KeyError ('unknown branch pattern for enable: ' + words[1])
219 elif words[0] == 'disable' :
220 if len (words) != 2 :
221 raise ValueError ('disable takes exactly one argument: ' + command)
222 used = False
223 for name in outputConfigs :
224 if re.match (words[1], name) :
225 outputConfigs[name].enabled = False
226 used = True
227 if not used and not optional and config.dataType() is not DataType.Data:
228 raise KeyError ('unknown branch pattern for disable: ' + words[1])
229 elif words[0] == 'rename' :
230 if len (words) != 3 :
231 raise ValueError ('rename takes exactly two arguments: ' + command)
232 used = False
233 for name in outputConfigs :
234 if re.match (words[1], name) :
235 new_name = re.sub (words[1], words[2], name)
236 outputConfigsRename[new_name] = copy.deepcopy(outputConfigs[name])
237 outputConfigs[name].enabled = False
238 used = True
239 if not used and not optional and config.dataType() is not DataType.Data:
240 raise KeyError ('unknown branch pattern for rename: ' + words[1])
241 else :
242 raise KeyError ('unknown command for "commands" option: ' + words[0])
243
244 # update the outputConfigs with renamed branches
245 outputConfigs.update(outputConfigsRename)
246
247 autoVars = set()
248 autoMetVars = set()
249 autoTruthMetVars = set()
250 for outputName, outputConfig in outputConfigs.items():
251 if outputConfig.enabled :
252 if config.isMetContainer (outputConfig.origContainerName) and outputConfig.prefix not in self.containersFullMET:
253 if "Truth" in outputConfig.origContainerName:
254 myVars = autoTruthMetVars
255 else:
256 myVars = autoMetVars
257 else :
258 myVars = autoVars
259 if outputConfig.noSys :
260 outputConfig.outputContainerName = outputConfig.outputContainerName.replace ('%SYS%', 'NOSYS')
261 outputConfig.variableName = outputConfig.variableName.replace ('%SYS%', 'NOSYS')
263 outputName += "_NOSYS"
264 else :
265 outputName += '_%SYS%'
266 branchDecl = f"{outputConfig.outputContainerName}.{outputConfig.variableName} -> {outputName}"
267 if outputConfig.auxType is not None :
268 branchDecl += f" type={outputConfig.auxType}"
269 if config.isMetContainer (outputConfig.origContainerName) and outputConfig.prefix not in self.containersFullMET:
270 if "Truth" in outputConfig.origContainerName:
271 branchDecl += f" metTerm={self.truthMetTermName}"
272 else:
273 branchDecl += f" metTerm={self.metTermName}"
274 myVars.add(branchDecl)
275
276 # Unified branch collection for all output formats
277 allBranches = set()
278 allBranches |= self.vars
279 allBranches |= autoVars
280 # Add MET branches
281 userMetVars = set()
282 if self.metVars:
283 for var in self.metVars:
284 userMetVars.add(var + " metTerm=" + self.metTermName)
285 allBranches |= userMetVars
286 allBranches |= autoMetVars
287 # Add truth MET branches (for MC)
288 userTruthMetVars = set()
289 if config.dataType() is not DataType.Data:
290 if self.truthMetVars:
291 for var in self.truthMetVars:
292 userTruthMetVars.add(var + " metTerm=" + self.truthMetTermName)
293 allBranches |= userTruthMetVars
294 allBranches |= autoTruthMetVars
295
296 # Create the output algorithm based on outputFormat
297 if self.outputFormat == 'RNTuple':
298 alg = config.createAlgorithm('CP::RNtupleTreeMakerAlg', 'RNtupleMaker')
299 alg.TreeName = self.treeName
300 alg.RootStreamName = self.streamName
301 alg.OutputStreamName = self.streamName
302 alg.NonContainers = list(self.nonContainers)
303
304 branchList = list(allBranches)
305 branchList.sort(key=self.branchSortOrder)
306 alg.Branches = branchList
307
308 return
309
310 # Add an ntuple dumper algorithm:
311 treeMaker = config.createAlgorithm( 'CP::TreeMakerAlg', 'TreeMaker' )
312 treeMaker.TreeName = self.treeName
313 treeMaker.RootStreamName = self.streamName
314 # the auto-flush setting still needs to be figured out
315 #treeMaker.TreeAutoFlush = 0
316
317 if self.vars or autoVars:
318 self.createOutputAlgs(config, 'NTupleMaker', self.vars | autoVars)
319
320 if self.metVars or autoMetVars:
321 self.createOutputAlgs(config, 'MetNTupleMaker', userMetVars | autoMetVars)
322
323 if config.dataType() is not DataType.Data and (self.truthMetVars or autoTruthMetVars):
324 self.createOutputAlgs(config, 'TruthMetNTupleMaker', userTruthMetVars | autoTruthMetVars)
325
326 treeFiller = config.createAlgorithm( 'CP::TreeFillerAlg', 'TreeFiller' )
327 treeFiller.TreeName = self.treeName
328 treeFiller.RootStreamName = self.streamName
329
330
331
333 """
334 For each container and for each selection, create a single pass variable in output NTuple,
335 which aggregates all the selections flag of the given selection. For example, this can include
336 pT, eta selections, some object ID selection, overlap removal, etc.
337 The goal is to have only one flag per object and working point in the output NTuple.
338 """
339 originalContainersSeen = []
340 for prefix in self.containers.keys() :
341 outputContainerName = self.containers[prefix]
342 containerName = config.getOutputContainerOrigin(outputContainerName)
343 if containerName in originalContainersSeen:
344 continue
345 else:
346 originalContainersSeen.append(containerName)
347
348 # EventInfo is one obvious example of a container that has no object selections
349 if containerName == 'EventInfo':
350 continue
351
352 # Get the selection names, except the systematic-dependent version of the FTAG
353 # selection flag, as it's already saved as a systematic-independent output branch
354 selectionNames = config.getSelectionNames(containerName, excludeFrom={'ftag'})
355 for selectionName in selectionNames:
356 # skip default selection
357 if selectionName == '':
358 continue
359 # skip selection coming from the Thinning block
360 if self.skipRedundantSelectionFlags and "outputSelect" in selectionName:
361 continue
362 self.makeSelectionSummaryAlg(config, containerName, selectionName)
363
364 def makeSelectionSummaryAlg(self, config, containerName, selectionName):
365 """
366 Schedule an algorithm to pick up all cut flags for a given selectionName.
367 The summary selection flag is written to output as selectionFlagPrefix_selectionName.
368 """
369 alg = config.createAlgorithm( 'CP::AsgSelectionAlg',
370 f'ObjectSelectionSummary_{containerName}_{selectionName}')
371 selectionDecoration = f'baselineSelection_{selectionName}_%SYS%'
372 alg.selectionDecoration = f'{selectionDecoration},as_char'
373 alg.particles = config.readName (containerName)
374 alg.preselection = config.getFullSelection (containerName, selectionName)
375 config.addOutputVar (containerName, selectionDecoration, self.selectionFlagPrefix + '_' + selectionName)
makeSelectionSummaryAlg(self, config, containerName, selectionName)
STL class.
std::vector< std::string > intersection(std::vector< std::string > &v1, std::vector< std::string > &v2)
std::string replace(std::string s, const std::string &s2, const std::string &s3)
Definition hcg.cxx:312