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