ATLAS Offline Software
Loading...
Searching...
No Matches
ConfigAccumulator.py
Go to the documentation of this file.
1# Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
2
3import AnaAlgorithm.DualUseConfig as DualUseConfig
4from AthenaConfiguration.Enums import LHCPeriod, FlagEnum
5import re
6
7import warnings
8import functools
9
10# warn about deprecations with a FutureWarning instead of a
11# DeprecationWarning, because DeprecatedWarning is not shown by default
12deprecationWarningCategory = FutureWarning
13def deprecated(reason: str = ""):
14 def decorator(func):
15 message = f"{func.__qualname__} is deprecated."
16 if reason:
17 message += " " + reason
18
19 @functools.wraps(func)
20 def wrapper(*args, **kwargs):
21 warnings.warn(
22 message,
23 category=deprecationWarningCategory,
24 stacklevel=2,
25 )
26 return func(*args, **kwargs)
27
28 return wrapper
29 return decorator
30
31class ExpertModeWarning(Warning):
32 """Warning raised when an expert-only configuration option is used."""
33 pass
34# Default filter: error out unless the user overrides
35if not any(f[0] == 'error' and f[2] is ExpertModeWarning for f in warnings.filters):
36 warnings.simplefilter('error', ExpertModeWarning)
37
38class DataType(FlagEnum):
39 """holds the various data types as an enum"""
40 Data = 'data'
41 FullSim = 'fullsim'
42 FastSim = 'fastsim'
43
44
46 """all the data for a given selection that has been registered
47
48 the bits argument is for backward compatibility, does nothing, and will be
49 removed in the future."""
50
51 def __init__ (self, selectionName, decoration,
52 *, bits=0, preselection=None, comesFrom = '',
53 writeToOutput=True) :
54 self.name = selectionName
55 self.decoration = decoration
56 if preselection is not None :
57 self.preselection = preselection
58 else :
59 self.preselection = (selectionName == '')
60 self.comesFrom = comesFrom
61 self.writeToOutput = writeToOutput
62
63
64
66 """all the data for a given variables in the output that has been registered"""
67
68 def __init__ (self, origContainerName, variableName,
69 *, noSys, enabled, auxType) :
70 self.origContainerName = origContainerName
72 self.variableName = variableName
73 self.noSys = noSys
74 self.enabled = enabled
75 self.auxType = auxType
76
77 def __repr__ (self):
78 return f'OutputConfig("{self.outputContainerName}.{self.variableName}" [enabled={self.enabled}])'
79
81 """all the auto-generated meta-configuration data for a single container
82
83 This tracks the naming of all temporary containers, as well as all the
84 selection decorations."""
85
86 def __init__ (self, name, sourceName, *, originalName = None, isMet = False, noSysSuffix) :
87 self.name = name
88 self.sourceName = sourceName
89 self.originalName = originalName
90 self.noSysSuffix = noSysSuffix
91 self.index = 0
92 self.maxIndex = None
93 self.viewIndex = 1
94 self.isMet = isMet
95 self.selections = []
96 self.outputs = {}
97 self.meta = {}
98
99 def currentName (self) :
100 if self.index == 0 :
101 if self.sourceName is None :
102 raise Exception ("should not get here, reading container name before created: " + self.name)
103 return self.sourceName
104 if self.maxIndex and self.index == self.maxIndex :
105 return self.systematicsName(self.name, noSysSuffix=self.noSysSuffix)
106 return self.systematicsName(f"{self.name}_STEP{self.index}", noSysSuffix=self.noSysSuffix)
107
108 @staticmethod
109 def systematicsName (name, *, noSysSuffix) :
110 """map an internal name to a name for systematics data handles
111
112 Right now this just means appending a _%SYS% to the name."""
113 if not noSysSuffix :
114 return name + "_%SYS%"
115 else :
116 return name
117
118 def nextPass (self) :
119 self.maxIndex = self.index
120 self.index = 0
121 self.viewIndex = 1
122 self.selections = []
123 self.outputs = {}
124 self.meta = {}
125
126
127
129 """a class that accumulates a configuration from blocks into an
130 algorithm sequence
131
132 This is used as argument to the ConfigurationBlock methods, which
133 need to be called in the correct order. This class will track all
134 meta-information that needs to be communicated between blocks
135 during configuration, and also add the created algorithms to the
136 sequence.
137
138 Use/access of containers in the event store is handled via
139 references that this class hands out. This happens in a separate
140 step before the algorithms are created, as the naming of
141 containers will depend on where in the chain the container is
142 used.
143 """
144
145 def __init__ (self, *, flags=None, algSeq=None, noSysSuffix=False, noSystematics=None, dataType=None, isPhyslite=None, geometry=None, dsid=0, campaign=None, runNumber=None, autoconfigFromFlags=None, dataYear=0):
146
147 # Historically we have used the identifier
148 # `autoconfigFromFlags`, but in the rest of the code base
149 # `flags` is used. So for now we allow either, and can hopefully
150 # at some point remove the former (21 Aug 25).
151 if autoconfigFromFlags is not None:
152 if flags is not None:
153 raise ValueError("Cannot pass both flags and autoconfigFromFlags arguments")
154 flags = autoconfigFromFlags
155 warnings.warn ('Using autoconfigFromFlags parameter is deprecated, use flags instead', category=deprecationWarningCategory, stacklevel=2)
156 self._flags = flags
157
158 # Historically the user was expected to pass in meta-data
159 # manually, which was a complete underestimate of the amount of
160 # meta-data needed. The current recommendation is to pass in a
161 # configuration flags object instead. The code below will raise
162 # an error if both are done, and if no configuration flags are
163 # passed in, it will try to create a flags object from the
164 # passed in parameters.
165 if self._flags is not None:
166 if dataType is not None:
167 raise ValueError("Cannot pass both dataType and flags/autoconfigFromFlags arguments")
168 if isPhyslite is not None:
169 raise ValueError("Cannot pass both isPhyslite and flags/autoconfigFromFlags arguments")
170 if geometry is not None:
171 raise ValueError("Cannot pass both geometry and flags/autoconfigFromFlags arguments")
172 if dsid != 0:
173 raise ValueError("Cannot pass both dsid and flags/autoconfigFromFlags arguments")
174 if campaign is not None:
175 raise ValueError("Cannot pass both campaign and flags/autoconfigFromFlags arguments")
176 if runNumber is not None:
177 raise ValueError("Cannot pass both runNumber and flags/autoconfigFromFlags arguments")
178 if dataYear != 0:
179 raise ValueError("Cannot pass both dataYear and flags/autoconfigFromFlags arguments")
180
181 if self._flags.Input.isMC:
182 if self._flags.Sim.ISF.Simulator.usesFastCaloSim():
183 dataType = DataType.FastSim
184 else:
185 dataType = DataType.FullSim
186 else:
187 dataType = DataType.Data
188 isPhyslite = 'StreamDAOD_PHYSLITE' in self._flags.Input.ProcessingTags
189 from TrigDecisionTool.TrigDecisionToolHelpers import (
190 getRun3NavigationContainerFromInput_forAnalysisBase)
191 hltSummary = getRun3NavigationContainerFromInput_forAnalysisBase(self._flags)
192 else:
193 warnings.warn ('it is deprecated to configure meta-data for analysis configuration manually, please read the configuration flags via the meta-data reader', category=deprecationWarningCategory, stacklevel=2)
194 from AthenaConfiguration.AllConfigFlags import initConfigFlags
195 flags = initConfigFlags()
196 if dataType is None:
197 raise ValueError ("need to specify dataType if flags are not set")
198 # legacy mappings of string arguments
199 if isinstance(dataType, str):
200 if dataType == 'mc':
201 dataType = DataType.FullSim
202 elif dataType == 'afii':
203 dataType = DataType.FastSim
204 else:
205 dataType = DataType(dataType)
206 if isPhyslite is None:
207 isPhyslite = False
208 if geometry is not None:
209 # allow possible string argument for `geometry` and convert it to enum
210 geometry = LHCPeriod(geometry)
211 if geometry is LHCPeriod.Run1:
212 raise ValueError ("invalid Run geometry: %s" % geometry.value)
213 flags.GeoModel.Run = geometry
214 if dsid != 0:
215 flags.Input.MCChannelNumber = dsid
216 if campaign is not None:
217 flags.Input.MCCampaign = campaign
218 if dataYear != 0:
219 flags.Input.DataYear = dataYear
220 if runNumber is None:
221 # not sure if we should just use a default run number
222 # here, or just report nothing
223 runNumber = 284500
224 flags.Input.RunNumbers = [runNumber]
225 hltSummary = 'HLTNav_Summary_DAODSlimmed'
226 flags.lock()
227 self._flags = flags
228
229 # These don't seem to have a direct equivalent in the
230 # configuration flags. For now I'm keeping them (21 Aug 25), but
231 # they might be replaced with something that is more directly in
232 # the configuration flags in the future.
233 self._dataType = dataType
234 self._isPhyslite = isPhyslite
235 self._hltSummary = hltSummary
236
237 # From here on, we are no longer dealing with flags or
238 # meta-data, but actual internal variables we need to manage the
239 # creation of components.
240 self._algSeq = algSeq
241 self._noSystematics = noSystematics
242 self._noSysSuffix = noSysSuffix
243 self._algPostfix = ''
246 self._pass = 0
247 self._algorithms = {}
248 self._currentAlg = None
249 self._selectionNameExpr = re.compile ('[A-Za-z_][A-Za-z_0-9]+')
250 self.setSourceName ('EventInfo', 'EventInfo')
252 self.CA = None
253
254 if DualUseConfig.isAthena:
255 from AthenaConfiguration.ComponentAccumulator import ComponentAccumulator
256 self.CA = ComponentAccumulator()
257 if algSeq is not None:
258 self.CA.addSequence(algSeq)
259 else:
260 if algSeq is None :
261 raise ValueError ("need to pass algSeq if not using ComponentAccumulator")
262
263 def noSystematics (self) :
264 """noSystematics flag used by CommonServices block"""
265 return self._noSystematics
266
267 @property
268 def flags (self) :
269 """Athena configuration flags"""
270 return self._flags
271
272 @deprecated("use the flags property instead")
273 def autoconfigFlags (self) :
274 """Athena configuration flags
275
276 This is a backward compatibility version of the flags property,
277 which is preferred."""
278 return self._flags
279
280 def dataType (self) :
281 """the data type we run on (data, fullsim, fastsim)"""
282 return self._dataType
283
284 def isPhyslite (self) :
285 """whether we run on PHYSLITE"""
286 return self._isPhyslite
287
288 def geometry (self) :
289 """the LHC Run period we run on"""
290 return self._flags.GeoModel.Run
291
292 def dsid(self) :
293 """the mcChannelNumber or DSID of the sample we run on"""
294 return self._flags.Input.MCChannelNumber
295
296 def campaign(self) :
297 """the MC campaign we run on"""
298 return self._flags.Input.MCCampaign
299
300 def runNumber(self) :
301 """the MC runNumber"""
302 return int(self._flags.Input.RunNumbers[0])
303
304 def dataYear(self) :
305 """for data, the corresponding year; for MC, zero"""
306 return self._flags.Input.DataYear
307
308 def generatorInfo(self) :
309 """the dictionary of MC generators and their versions for the sample we run on"""
310 return self._flags.Input.GeneratorsInfo
311
312 def hltSummary(self) :
313 """the HLTSummary configuration to be used for the trigger decision tool"""
314 return self._hltSummary
315
316 def algPostfix (self) :
317 """the current postfix to be appended to algorithm names
318
319 Blocks should not call this directly, but rather implement the
320 instanceName method, which will be used to generate the postfix
321 automatically."""
322 return self._algPostfix
323
324 def setAlgPostfix (self, postfix : str) :
325 """set the current postfix to be appended to algorithm names
326
327 Blocks should not call this directly, but rather implement the
328 instanceName method, which will be used to generate the postfix
329 automatically."""
330 # make sure the postfix matches the expected format ([_a-zA-Z0-9]*)
331 if re.compile ('^[_a-zA-Z0-9]*$').match (postfix) is None :
332 raise ValueError ('invalid algorithm postfix: ' + postfix)
333 if postfix == '' :
334 self._algPostfix = ''
335 elif postfix[0] != '_' :
336 self._algPostfix = '_' + postfix
337 else :
338 self._algPostfix = postfix
339
340 def getAlgorithm (self, name : str):
341 """get the algorithm with the given name
342
343 Despite the name this will also return services and tools. It is
344 mostly meant for internal use, particularly for the property
345 overrides."""
346 name = name + self._algPostfix
347 if name not in self._algorithms:
348 return None
349 return self._algorithms[name]
350
351 def createAlgorithm (self, type, name, reentrant=False) :
352 """create a new algorithm and register it as the current algorithm"""
353 name = name + self._algPostfix
354 if self._pass == 0 :
355 if name in self._algorithms :
356 raise Exception ('duplicate algorithms: ' + name + ' with algPostfix=' + self._algPostfix)
357 if reentrant:
358 alg = DualUseConfig.createReentrantAlgorithm (type, name)
359 else:
360 alg = DualUseConfig.createAlgorithm (type, name)
361
362 if DualUseConfig.isAthena:
363 if self._algSeq is not None:
364 self.CA.addEventAlgo(alg,self._algSeq.name)
365 else :
366 self.CA.addEventAlgo(alg)
367 else:
368 self._algSeq += alg
369
370 self._algorithms[name] = alg
371 self._currentAlg = alg
372 return alg
373 else :
374 if name not in self._algorithms :
375 raise Exception ('unknown algorithm requested: ' + name)
376 self._currentAlg = self._algorithms[name]
377 if self.CA and self._currentAlg != self.CA.getEventAlgo(name) :
378 raise Exception ('change to algorithm object: ' + name)
379 return self._algorithms[name]
380
381
382 def createService (self, type, name) :
383 '''create a new service and register it as the "current algorithm"'''
384 if self._pass == 0 :
385 if name in self._algorithms :
386 raise Exception ('duplicate service: ' + name)
387 service = DualUseConfig.createService (type, name)
388 # Avoid importing AthenaCommon.AppMgr in a CA Athena job
389 # as it modifies Gaudi behaviour
390 if DualUseConfig.isAthena:
391 self.CA.addService(service)
392 else:
393 # We're not, so let's remember this as a "normal" algorithm:
394 self._algSeq += service
395 self._algorithms[name] = service
396 self._currentAlg = service
397 return service
398 else :
399 if name not in self._algorithms :
400 raise Exception ('unknown service requested: ' + name)
401 self._currentAlg = self._algorithms[name]
402 return self._algorithms[name]
403
404
405 def createPublicTool (self, type, name) :
406 '''create a new public tool and register it as the "current algorithm"'''
407 if self._pass == 0 :
408 if name in self._algorithms :
409 raise Exception ('duplicate public tool: ' + name)
410 tool = DualUseConfig.createPublicTool (type, name)
411 # Avoid importing AthenaCommon.AppMgr in a CA Athena job
412 # as it modifies Gaudi behaviour
413 if DualUseConfig.isAthena:
414 self.CA.addPublicTool(tool)
415 else:
416 # We're not, so let's remember this as a "normal" algorithm:
417 self._algSeq += tool
418 self._algorithms[name] = tool
419 self._currentAlg = tool
420 return tool
421 else :
422 if name not in self._algorithms :
423 raise Exception ('unknown public tool requested: ' + name)
424 self._currentAlg = self._algorithms[name]
425 return self._algorithms[name]
426
427
428 def addPrivateTool (self, propertyName, toolType) :
429 """add a private tool to the current algorithm"""
430 if self._pass == 0 :
431 DualUseConfig.addPrivateTool (self._currentAlg, propertyName, toolType)
432
433
434 def setSourceName (self, containerName, sourceName,
435 *, originalName = None, isMet = False) :
436 """set the (default) name of the source/original container
437
438 This is essentially meant to allow using e.g. the muon
439 configuration and the user not having to manually specify that
440 they want to use the Muons/AnalysisMuons container from the
441 input file.
442
443 In addition it allows to set the original name of the
444 container (which may be different from the source name), which
445 is mostly/exclusively used for jet containers, so that
446 subsequent configurations know which jet container they
447 operate on.
448 """
449 if containerName not in self._containerConfig :
450 self._containerConfig[containerName] = ContainerConfig (containerName, sourceName, noSysSuffix = self._noSysSuffix, originalName = originalName, isMet = isMet)
451
452
453 def writeName (self, containerName, *, isMet=None) :
454 """register that the given container will be made and return
455 its name"""
456 if containerName not in self._containerConfig :
457 self._containerConfig[containerName] = ContainerConfig (containerName, sourceName = None, noSysSuffix = self._noSysSuffix)
458 if self._containerConfig[containerName].sourceName is not None :
459 raise Exception ("trying to write container configured for input: " + containerName)
460 if self._containerConfig[containerName].index != 0 :
461 raise Exception ("trying to write container twice: " + containerName)
462 self._containerConfig[containerName].index += 1
463 if isMet is not None :
464 self._containerConfig[containerName].isMet = isMet
465 return self._containerConfig[containerName].currentName()
466
467
468 def readName (self, containerName) :
469 """get the name of the "current copy" of the given container
470
471 As extra copies get created during processing this will track
472 the correct name of the current copy. Optionally one can pass
473 in the name of the container before the first copy.
474 """
475 if containerName not in self._containerConfig :
476 raise Exception ("no source container for: " + containerName)
477 return self._containerConfig[containerName].currentName()
478
479
480 def copyName (self, containerName) :
481 """register that a copy of the container will be made and return
482 its name"""
483 if containerName not in self._containerConfig :
484 raise Exception ("unknown container: " + containerName)
485 self._containerConfig[containerName].index += 1
486 return self._containerConfig[containerName].currentName()
487
488
489 def wantCopy (self, containerName) :
490 """ask whether we want/need a copy of the container
491
492 This usually only happens if no copy of the container has been
493 made yet and the copy is needed to allow modifications, etc.
494 """
495 if containerName not in self._containerConfig :
496 raise Exception ("no source container for: " + containerName)
497 return self._containerConfig[containerName].index == 0
498
499
500 def originalName (self, containerName) :
501 """get the "original" name of the given container
502
503 This is mostly/exclusively used for jet containers, so that
504 subsequent configurations know which jet container they
505 operate on.
506 """
507 if containerName not in self._containerConfig :
508 raise Exception ("container unknown: " + containerName)
509 result = self._containerConfig[containerName].originalName
510 if result is None :
511 raise Exception ("no original name for: " + containerName)
512 return result
513
514 def getContainerMeta (self, containerName, metaField, defaultValue=None, *, failOnMiss=False) :
515 """get the meta information for the given container
516
517 This is used to pass down meta-information from the
518 configuration to the algorithms.
519 """
520 if containerName not in self._containerConfig :
521 raise Exception ("container unknown: " + containerName)
522 if metaField in self._containerConfig[containerName].meta :
523 return self._containerConfig[containerName].meta[metaField]
524 if failOnMiss :
525 raise Exception ('unknown meta-field' + metaField + ' on container ' + containerName)
526 return defaultValue
527
528 def setContainerMeta (self, containerName, metaField, value, *, allowOverwrite=False) :
529 """set the meta information for the given container
530
531 This is used to pass down meta-information from the
532 configuration to the algorithms.
533 """
534 if containerName not in self._containerConfig :
535 raise Exception ("container unknown: " + containerName)
536 if not allowOverwrite and metaField in self._containerConfig[containerName].meta :
537 raise Exception ('duplicate meta-field' + metaField + ' on container ' + containerName)
538 self._containerConfig[containerName].meta[metaField] = value
539
540 def isMetContainer (self, containerName) :
541 """whether the given container is registered as a MET container
542
543 This is mostly/exclusively used for determining whether to
544 write out the whole container or just a single MET term.
545 """
546 if containerName not in self._containerConfig :
547 raise Exception ("container unknown: " + containerName)
548 return self._containerConfig[containerName].isMet
549
550
551 def readNameAndSelection (self, containerName, *, excludeFrom = None) :
552 """get the name of the "current copy" of the given container, and the
553 selection string
554
555 This is mostly meant for MET and OR for whom the actual object
556 selection is relevant, and which as such allow to pass in the
557 working point as "ObjectName.WorkingPoint".
558 """
559 split = containerName.split (".")
560 if len(split) == 1 :
561 objectName = split[0]
562 selectionName = ''
563 elif len(split) == 2 :
564 objectName = split[0]
565 selectionName = split[1]
566 else :
567 raise Exception ('invalid object selection name: ' + containerName)
568 return self.readName (objectName), self.getFullSelection (objectName, selectionName, excludeFrom=excludeFrom)
569
570
571 def nextPass (self) :
572 """switch to the next configuration pass
573
574 Configuration happens in two steps, with all the blocks processed
575 twice. This switches from the first to the second pass.
576 """
577 if self._pass != 0 :
578 raise Exception ("already performed final pass")
579 for name in self._containerConfig :
580 self._containerConfig[name].nextPass ()
581 self._pass = 1
582 self._currentAlg = None
583 self._outputContainers = {}
584
585
586 def getPreselection (self, containerName, selectionName, *, asList = False) :
587
588 """get the preselection string for the given selection on the given
589 container
590 """
591 if selectionName != '' and not self._selectionNameExpr.fullmatch (selectionName) :
592 raise ValueError ('invalid selection name: ' + selectionName)
593 if containerName not in self._containerConfig :
594 return ""
595 config = self._containerConfig[containerName]
596 decorations = []
597 for selection in config.selections :
598 if (selection.name == '' or selection.name == selectionName) and \
599 selection.preselection :
600 decorations += [selection.decoration]
601 if asList :
602 return decorations
603 else :
604 return '&&'.join (decorations)
605
606
607 def getFullSelection (self, containerName, selectionName,
608 *, skipBase = False, excludeFrom = None) :
609
610 """get the selection string for the given selection on the given
611 container
612
613 This can handle both individual selections or selection
614 expressions (e.g. `loose||tight`) with the later being
615 properly expanded. Either way the base selection (i.e. the
616 selection without a name) will always be applied on top.
617
618 containerName --- the container the selection is defined on
619 selectionName --- the name of the selection, or a selection
620 expression based on multiple named selections
621 skipBase --- will avoid the base selection, and should normally
622 not be used by the end-user.
623 excludeFrom --- a set of string names of selection sources to exclude
624 e.g. to exclude OR selections from MET
625 """
626 if "." in containerName:
627 raise ValueError (f'invalid containerName argument: {containerName} , it contains a "." '
628 'which is used to indicate container+selection. You should only pass the container.')
629 if containerName not in self._containerConfig :
630 return ""
631
632 if excludeFrom is None :
633 excludeFrom = set()
634 elif not isinstance(excludeFrom, set) :
635 raise ValueError ('invalid excludeFrom argument (need set of strings): ' + str(excludeFrom))
636
637 # Check if this is actually a selection expression,
638 # e.g. `A||B` and if so translate it into a complex expression
639 # for the user. I'm not trying to do any complex syntax
640 # recognition, but instead just produce an expression that the
641 # C++ parser ought to be able to read.
642 if selectionName != '' and \
643 not self._selectionNameExpr.fullmatch (selectionName) :
644 result = ''
645 while selectionName != '' :
646 match = self._selectionNameExpr.match (selectionName)
647 if not match :
648 result += selectionName[0]
649 selectionName = selectionName[1:]
650 else :
651 subname = match.group(0)
652 subresult = self.getFullSelection (containerName, subname, skipBase = True, excludeFrom=excludeFrom)
653 if subresult != '' :
654 result += '(' + subresult + ')'
655 else :
656 result += 'true'
657 selectionName = selectionName[len(subname):]
658 subresult = self.getFullSelection (containerName, '', excludeFrom=excludeFrom)
659 if subresult != '' :
660 result = subresult + '&&(' + result + ')'
661 return '(' + result + ')' if result !='' else ''
662
663 config = self._containerConfig[containerName]
664 decorations = []
665 hasSelectionName = False
666 for selection in config.selections :
667 if ((selection.name == '' and not skipBase) or selection.name == selectionName) and (selection.comesFrom not in excludeFrom) :
668 decorations += [selection.decoration]
669 if selection.name == selectionName :
670 hasSelectionName = True
671 if not hasSelectionName and selectionName != '' :
672 raise KeyError ('invalid selection name: ' + containerName + '.' + selectionName)
673 return '&&'.join (decorations)
674
675
676 def getSelectionCutFlow (self, containerName, selectionName) :
677
678 """get the individual selections as a list for producing the cutflow for
679 the given selection on the given container
680
681 This can only handle individual selections, not selection
682 expressions (e.g. `loose||tight`).
683
684 """
685 if containerName not in self._containerConfig :
686 return []
687
688 # Check if this is actually a selection expression,
689 # e.g. `A||B` and if so translate it into a complex expression
690 # for the user. I'm not trying to do any complex syntax
691 # recognition, but instead just produce an expression that the
692 # C++ parser ought to be able to read.
693 if selectionName != '' and \
694 not self._selectionNameExpr.fullmatch (selectionName) :
695 raise ValueError ('not allowed to do cutflow on selection expression: ' + selectionName)
696
697 config = self._containerConfig[containerName]
698 decorations = []
699 for selection in config.selections :
700 if (selection.name == '' or selection.name == selectionName) :
701 decorations += [selection.decoration]
702 return decorations
703
704
705 def addEventCutFlow (self, selection, decorations) :
706
707 """register a new event cutflow, adding it to the dictionary with key 'selection'
708 and value 'decorations', a list of decorated selections
709 """
710 if self._pass == 0:
711 if selection in self._eventcutflow.keys():
712 raise ValueError ('the event cutflow dictionary already contains an entry ' + selection)
713 else:
714 self._eventcutflow[selection] = decorations
715
716
717 def getEventCutFlow (self, selection) :
718
719 """get the list of decorated selections for an event cutflow, corresponding to
720 key 'selection'
721 """
722 return self._eventcutflow[selection]
723
724
725 def addSelection (self, containerName, selectionName, decoration,
726 **kwargs) :
727 """add another selection decoration to the selection of the given
728 name for the given container"""
729 if selectionName != '' and not self._selectionNameExpr.fullmatch (selectionName) :
730 raise ValueError ('invalid selection name: ' + selectionName)
731 if containerName not in self._containerConfig :
732 self._containerConfig[containerName] = ContainerConfig (containerName, containerName, noSysSuffix=self._noSysSuffix)
733 config = self._containerConfig[containerName]
734 selection = SelectionConfig (selectionName, decoration, **kwargs)
735 config.selections.append (selection)
736
737
738 def addOutputContainer (self, containerName, outputContainerName) :
739 """register a copy of a container used in outputs"""
740 if containerName not in self._containerConfig :
741 raise KeyError ("container unknown: " + containerName)
742 if outputContainerName in self._outputContainers :
743 raise KeyError ("duplicate output container name: " + outputContainerName)
744 self._outputContainers[outputContainerName] = containerName
745
746
747 def checkOutputContainer (self, containerName) :
748 """check whether a given container has been registered in outputs"""
749 return containerName in self._outputContainers.values()
750
751
752 def getOutputContainerOrigin (self, outputContainerName) :
753 """Get the name of the actual container, for which an output is registered"""
754 try:
755 return self._outputContainers[outputContainerName]
756 except KeyError:
757 try:
758 return self._containerConfig[outputContainerName].name
759 except KeyError:
760 raise KeyError ("output container unknown: " + outputContainerName)
761
762
763 def addOutputVar (self, containerName, variableName, outputName,
764 *, noSys=False, enabled=True, auxType=None) :
765 """add an output variable for the given container to the output
766 """
767
768 if containerName not in self._containerConfig :
769 raise KeyError ("container unknown: " + containerName)
770 baseConfig = self._containerConfig[containerName].outputs
771 if outputName in baseConfig :
772 raise KeyError ("duplicate output variable name: " + outputName)
773 config = OutputConfig (containerName, variableName, noSys=noSys, enabled=enabled, auxType=auxType)
774 baseConfig[outputName] = config
775
776
777 def getOutputVars (self, containerName) :
778 """get the output variables for the given container"""
779 if containerName in self._outputContainers :
780 containerName = self._outputContainers[containerName]
781 if containerName not in self._containerConfig :
782 raise KeyError ("unknown container for output: " + containerName)
783 return self._containerConfig[containerName].outputs
784
785
786 def getSelectionNames (self, containerName, excludeFrom = None) :
787 """Retrieve set of unique selections defined for a given container"""
788 if containerName not in self._containerConfig :
789 return []
790 if excludeFrom is None:
791 excludeFrom = set()
792 elif not isinstance(excludeFrom, set) :
793 raise ValueError ('invalid excludeFrom argument (need set of strings): ' + str(excludeFrom))
794
795 config = self._containerConfig[containerName]
796 # because cuts are registered individually, selection names can repeat themselves
797 # but we are interested in unique names only
798 selectionNames = set()
799 for selection in config.selections:
800 if selection.comesFrom in excludeFrom:
801 continue
802 # skip flags which should be disabled in output
803 if selection.writeToOutput:
804 selectionNames.add(selection.name)
805 return selectionNames
setSourceName(self, containerName, sourceName, *, originalName=None, isMet=False)
__init__(self, *, flags=None, algSeq=None, noSysSuffix=False, noSystematics=None, dataType=None, isPhyslite=None, geometry=None, dsid=0, campaign=None, runNumber=None, autoconfigFromFlags=None, dataYear=0)
__init__(self, name, sourceName, *, originalName=None, isMet=False, noSysSuffix)
__init__(self, origContainerName, variableName, *, noSys, enabled, auxType)
__init__(self, selectionName, decoration, *, bits=0, preselection=None, comesFrom='', writeToOutput=True)