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