ATLAS Offline Software
Loading...
Searching...
No Matches
ConfigAccumulator.py
Go to the documentation of this file.
1# Copyright (C) 2002-2024 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
253 # If we are in an Athena environment with ComponentAccumulator configuration
254 # then the AlgSequence, which is Gaudi.AthSequencer, does not support '+=',
255 # and we in any case want to produce an output ComponentAccumulator
256 self.CA = None
257 if DualUseConfig.useComponentAccumulator:
258 from AthenaConfiguration.ComponentAccumulator import ComponentAccumulator
259 self.CA = ComponentAccumulator()
260 # if we have a component accumulator the user is not required to pass
261 # in a sequence, but if they do let's add it
262 if algSeq :
263 self.CA.addSequence(algSeq)
264 else :
265 if algSeq is None :
266 raise ValueError ("need to pass algSeq if not using ComponentAccumulator")
267
268
269 def noSystematics (self) :
270 """noSystematics flag used by CommonServices block"""
271 return self._noSystematics
272
273 @property
274 def flags (self) :
275 """Athena configuration flags"""
276 return self._flags
277
278 @deprecated("use the flags property instead")
279 def autoconfigFlags (self) :
280 """Athena configuration flags
281
282 This is a backward compatibility version of the flags property,
283 which is preferred."""
284 return self._flags
285
286 def dataType (self) :
287 """the data type we run on (data, fullsim, fastsim)"""
288 return self._dataType
289
290 def isPhyslite (self) :
291 """whether we run on PHYSLITE"""
292 return self._isPhyslite
293
294 def geometry (self) :
295 """the LHC Run period we run on"""
296 return self._flags.GeoModel.Run
297
298 def dsid(self) :
299 """the mcChannelNumber or DSID of the sample we run on"""
300 return self._flags.Input.MCChannelNumber
301
302 def campaign(self) :
303 """the MC campaign we run on"""
304 return self._flags.Input.MCCampaign
305
306 def runNumber(self) :
307 """the MC runNumber"""
308 return int(self._flags.Input.RunNumbers[0])
309
310 def dataYear(self) :
311 """for data, the corresponding year; for MC, zero"""
312 return self._flags.Input.DataYear
313
314 def generatorInfo(self) :
315 """the dictionary of MC generators and their versions for the sample we run on"""
316 return self._flags.Input.GeneratorsInfo
317
318 def hltSummary(self) :
319 """the HLTSummary configuration to be used for the trigger decision tool"""
320 return self._hltSummary
321
322 def algPostfix (self) :
323 """the current postfix to be appended to algorithm names
324
325 Blocks should not call this directly, but rather implement the
326 instanceName method, which will be used to generate the postfix
327 automatically."""
328 return self._algPostfix
329
330 def setAlgPostfix (self, postfix : str) :
331 """set the current postfix to be appended to algorithm names
332
333 Blocks should not call this directly, but rather implement the
334 instanceName method, which will be used to generate the postfix
335 automatically."""
336 # make sure the postfix matches the expected format ([_a-zA-Z0-9]*)
337 if re.compile ('^[_a-zA-Z0-9]*$').match (postfix) is None :
338 raise ValueError ('invalid algorithm postfix: ' + postfix)
339 if postfix == '' :
340 self._algPostfix = ''
341 elif postfix[0] != '_' :
342 self._algPostfix = '_' + postfix
343 else :
344 self._algPostfix = postfix
345
346 def getAlgorithm (self, name : str):
347 """get the algorithm with the given name
348
349 Despite the name this will also return services and tools. It is
350 mostly meant for internal use, particularly for the property
351 overrides."""
352 name = name + self._algPostfix
353 if name not in self._algorithms:
354 return None
355 return self._algorithms[name]
356
357 def createAlgorithm (self, type, name, reentrant=False) :
358 """create a new algorithm and register it as the current algorithm"""
359 name = name + self._algPostfix
360 if self._pass == 0 :
361 if name in self._algorithms :
362 raise Exception ('duplicate algorithms: ' + name + ' with algPostfix=' + self._algPostfix)
363 if reentrant:
364 alg = DualUseConfig.createReentrantAlgorithm (type, name)
365 else:
366 alg = DualUseConfig.createAlgorithm (type, name)
367
368 if DualUseConfig.useComponentAccumulator:
369 if self._algSeq :
370 self.CA.addEventAlgo(alg,self._algSeq.name)
371 else :
372 self.CA.addEventAlgo(alg)
373 else:
374 self._algSeq += alg
375 self._algorithms[name] = alg
376 self._currentAlg = alg
377 return alg
378 else :
379 if name not in self._algorithms :
380 raise Exception ('unknown algorithm requested: ' + name)
381 self._currentAlg = self._algorithms[name]
382 if self.CA and self._currentAlg != self.CA.getEventAlgo(name) :
383 raise Exception ('change to algorithm object: ' + name)
384 return self._algorithms[name]
385
386
387 def createService (self, type, name) :
388 '''create a new service and register it as the "current algorithm"'''
389 if self._pass == 0 :
390 if name in self._algorithms :
391 raise Exception ('duplicate service: ' + name)
392 service = DualUseConfig.createService (type, name)
393 # Avoid importing AthenaCommon.AppMgr in a CA Athena job
394 # as it modifies Gaudi behaviour
395 if DualUseConfig.isAthena:
396 if DualUseConfig.useComponentAccumulator:
397 self.CA.addService(service)
398 else:
399 # We're not, so let's remember this as a "normal" algorithm:
400 self._algSeq += service
401 self._algorithms[name] = service
402 self._currentAlg = service
403 return service
404 else :
405 if name not in self._algorithms :
406 raise Exception ('unknown service requested: ' + name)
407 self._currentAlg = self._algorithms[name]
408 return self._algorithms[name]
409
410
411 def createPublicTool (self, type, name) :
412 '''create a new public tool and register it as the "current algorithm"'''
413 if self._pass == 0 :
414 if name in self._algorithms :
415 raise Exception ('duplicate public tool: ' + name)
416 tool = DualUseConfig.createPublicTool (type, name)
417 # Avoid importing AthenaCommon.AppMgr in a CA Athena job
418 # as it modifies Gaudi behaviour
419 if DualUseConfig.isAthena:
420 if DualUseConfig.useComponentAccumulator:
421 self.CA.addPublicTool(tool)
422 else:
423 # We're not, so let's remember this as a "normal" algorithm:
424 self._algSeq += tool
425 self._algorithms[name] = tool
426 self._currentAlg = tool
427 return tool
428 else :
429 if name not in self._algorithms :
430 raise Exception ('unknown public tool requested: ' + name)
431 self._currentAlg = self._algorithms[name]
432 return self._algorithms[name]
433
434
435 def addPrivateTool (self, propertyName, toolType) :
436 """add a private tool to the current algorithm"""
437 if self._pass == 0 :
438 DualUseConfig.addPrivateTool (self._currentAlg, propertyName, toolType)
439
440
441 def setSourceName (self, containerName, sourceName,
442 *, originalName = None, isMet = False) :
443 """set the (default) name of the source/original container
444
445 This is essentially meant to allow using e.g. the muon
446 configuration and the user not having to manually specify that
447 they want to use the Muons/AnalysisMuons container from the
448 input file.
449
450 In addition it allows to set the original name of the
451 container (which may be different from the source name), which
452 is mostly/exclusively used for jet containers, so that
453 subsequent configurations know which jet container they
454 operate on.
455 """
456 if containerName not in self._containerConfig :
457 self._containerConfig[containerName] = ContainerConfig (containerName, sourceName, noSysSuffix = self._noSysSuffix, originalName = originalName, isMet = isMet)
458
459
460 def writeName (self, containerName, *, isMet=None) :
461 """register that the given container will be made and return
462 its name"""
463 if containerName not in self._containerConfig :
464 self._containerConfig[containerName] = ContainerConfig (containerName, sourceName = None, noSysSuffix = self._noSysSuffix)
465 if self._containerConfig[containerName].sourceName is not None :
466 raise Exception ("trying to write container configured for input: " + containerName)
467 if self._containerConfig[containerName].index != 0 :
468 raise Exception ("trying to write container twice: " + containerName)
469 self._containerConfig[containerName].index += 1
470 if isMet is not None :
471 self._containerConfig[containerName].isMet = isMet
472 return self._containerConfig[containerName].currentName()
473
474
475 def readName (self, containerName) :
476 """get the name of the "current copy" of the given container
477
478 As extra copies get created during processing this will track
479 the correct name of the current copy. Optionally one can pass
480 in the name of the container before the first copy.
481 """
482 if containerName not in self._containerConfig :
483 raise Exception ("no source container for: " + containerName)
484 return self._containerConfig[containerName].currentName()
485
486
487 def copyName (self, containerName) :
488 """register that a copy of the container will be made and return
489 its name"""
490 if containerName not in self._containerConfig :
491 raise Exception ("unknown container: " + containerName)
492 self._containerConfig[containerName].index += 1
493 return self._containerConfig[containerName].currentName()
494
495
496 def wantCopy (self, containerName) :
497 """ask whether we want/need a copy of the container
498
499 This usually only happens if no copy of the container has been
500 made yet and the copy is needed to allow modifications, etc.
501 """
502 if containerName not in self._containerConfig :
503 raise Exception ("no source container for: " + containerName)
504 return self._containerConfig[containerName].index == 0
505
506
507 def originalName (self, containerName) :
508 """get the "original" name of the given container
509
510 This is mostly/exclusively used for jet containers, so that
511 subsequent configurations know which jet container they
512 operate on.
513 """
514 if containerName not in self._containerConfig :
515 raise Exception ("container unknown: " + containerName)
516 result = self._containerConfig[containerName].originalName
517 if result is None :
518 raise Exception ("no original name for: " + containerName)
519 return result
520
521 def getContainerMeta (self, containerName, metaField, defaultValue=None, *, failOnMiss=False) :
522 """get the meta information for the given container
523
524 This is used to pass down meta-information from the
525 configuration to the algorithms.
526 """
527 if containerName not in self._containerConfig :
528 raise Exception ("container unknown: " + containerName)
529 if metaField in self._containerConfig[containerName].meta :
530 return self._containerConfig[containerName].meta[metaField]
531 if failOnMiss :
532 raise Exception ('unknown meta-field' + metaField + ' on container ' + containerName)
533 return defaultValue
534
535 def setContainerMeta (self, containerName, metaField, value, *, allowOverwrite=False) :
536 """set the meta information for the given container
537
538 This is used to pass down meta-information from the
539 configuration to the algorithms.
540 """
541 if containerName not in self._containerConfig :
542 raise Exception ("container unknown: " + containerName)
543 if not allowOverwrite and metaField in self._containerConfig[containerName].meta :
544 raise Exception ('duplicate meta-field' + metaField + ' on container ' + containerName)
545 self._containerConfig[containerName].meta[metaField] = value
546
547 def isMetContainer (self, containerName) :
548 """whether the given container is registered as a MET container
549
550 This is mostly/exclusively used for determining whether to
551 write out the whole container or just a single MET term.
552 """
553 if containerName not in self._containerConfig :
554 raise Exception ("container unknown: " + containerName)
555 return self._containerConfig[containerName].isMet
556
557
558 def readNameAndSelection (self, containerName, *, excludeFrom = None) :
559 """get the name of the "current copy" of the given container, and the
560 selection string
561
562 This is mostly meant for MET and OR for whom the actual object
563 selection is relevant, and which as such allow to pass in the
564 working point as "ObjectName.WorkingPoint".
565 """
566 split = containerName.split (".")
567 if len(split) == 1 :
568 objectName = split[0]
569 selectionName = ''
570 elif len(split) == 2 :
571 objectName = split[0]
572 selectionName = split[1]
573 else :
574 raise Exception ('invalid object selection name: ' + containerName)
575 return self.readName (objectName), self.getFullSelection (objectName, selectionName, excludeFrom=excludeFrom)
576
577
578 def nextPass (self) :
579 """switch to the next configuration pass
580
581 Configuration happens in two steps, with all the blocks processed
582 twice. This switches from the first to the second pass.
583 """
584 if self._pass != 0 :
585 raise Exception ("already performed final pass")
586 for name in self._containerConfig :
587 self._containerConfig[name].nextPass ()
588 self._pass = 1
589 self._currentAlg = None
590 self._outputContainers = {}
591
592
593 def getPreselection (self, containerName, selectionName, *, asList = False) :
594
595 """get the preselection string for the given selection on the given
596 container
597 """
598 if selectionName != '' and not self._selectionNameExpr.fullmatch (selectionName) :
599 raise ValueError ('invalid selection name: ' + selectionName)
600 if containerName not in self._containerConfig :
601 return ""
602 config = self._containerConfig[containerName]
603 decorations = []
604 for selection in config.selections :
605 if (selection.name == '' or selection.name == selectionName) and \
606 selection.preselection :
607 decorations += [selection.decoration]
608 if asList :
609 return decorations
610 else :
611 return '&&'.join (decorations)
612
613
614 def getFullSelection (self, containerName, selectionName,
615 *, skipBase = False, excludeFrom = None) :
616
617 """get the selection string for the given selection on the given
618 container
619
620 This can handle both individual selections or selection
621 expressions (e.g. `loose||tight`) with the later being
622 properly expanded. Either way the base selection (i.e. the
623 selection without a name) will always be applied on top.
624
625 containerName --- the container the selection is defined on
626 selectionName --- the name of the selection, or a selection
627 expression based on multiple named selections
628 skipBase --- will avoid the base selection, and should normally
629 not be used by the end-user.
630 excludeFrom --- a set of string names of selection sources to exclude
631 e.g. to exclude OR selections from MET
632 """
633 if "." in containerName:
634 raise ValueError (f'invalid containerName argument: {containerName} , it contains a "." '
635 'which is used to indicate container+selection. You should only pass the container.')
636 if containerName not in self._containerConfig :
637 return ""
638
639 if excludeFrom is None :
640 excludeFrom = set()
641 elif not isinstance(excludeFrom, set) :
642 raise ValueError ('invalid excludeFrom argument (need set of strings): ' + str(excludeFrom))
643
644 # Check if this is actually a selection expression,
645 # e.g. `A||B` and if so translate it into a complex expression
646 # for the user. I'm not trying to do any complex syntax
647 # recognition, but instead just produce an expression that the
648 # C++ parser ought to be able to read.
649 if selectionName != '' and \
650 not self._selectionNameExpr.fullmatch (selectionName) :
651 result = ''
652 while selectionName != '' :
653 match = self._selectionNameExpr.match (selectionName)
654 if not match :
655 result += selectionName[0]
656 selectionName = selectionName[1:]
657 else :
658 subname = match.group(0)
659 subresult = self.getFullSelection (containerName, subname, skipBase = True, excludeFrom=excludeFrom)
660 if subresult != '' :
661 result += '(' + subresult + ')'
662 else :
663 result += 'true'
664 selectionName = selectionName[len(subname):]
665 subresult = self.getFullSelection (containerName, '', excludeFrom=excludeFrom)
666 if subresult != '' :
667 result = subresult + '&&(' + result + ')'
668 return '(' + result + ')' if result !='' else ''
669
670 config = self._containerConfig[containerName]
671 decorations = []
672 hasSelectionName = False
673 for selection in config.selections :
674 if ((selection.name == '' and not skipBase) or selection.name == selectionName) and (selection.comesFrom not in excludeFrom) :
675 decorations += [selection.decoration]
676 if selection.name == selectionName :
677 hasSelectionName = True
678 if not hasSelectionName and selectionName != '' :
679 raise KeyError ('invalid selection name: ' + containerName + '.' + selectionName)
680 return '&&'.join (decorations)
681
682
683 def getSelectionCutFlow (self, containerName, selectionName) :
684
685 """get the individual selections as a list for producing the cutflow for
686 the given selection on the given container
687
688 This can only handle individual selections, not selection
689 expressions (e.g. `loose||tight`).
690
691 """
692 if containerName not in self._containerConfig :
693 return []
694
695 # Check if this is actually a selection expression,
696 # e.g. `A||B` and if so translate it into a complex expression
697 # for the user. I'm not trying to do any complex syntax
698 # recognition, but instead just produce an expression that the
699 # C++ parser ought to be able to read.
700 if selectionName != '' and \
701 not self._selectionNameExpr.fullmatch (selectionName) :
702 raise ValueError ('not allowed to do cutflow on selection expression: ' + selectionName)
703
704 config = self._containerConfig[containerName]
705 decorations = []
706 for selection in config.selections :
707 if (selection.name == '' or selection.name == selectionName) :
708 decorations += [selection.decoration]
709 return decorations
710
711
712 def addEventCutFlow (self, selection, decorations) :
713
714 """register a new event cutflow, adding it to the dictionary with key 'selection'
715 and value 'decorations', a list of decorated selections
716 """
717 if self._pass == 0:
718 if selection in self._eventcutflow.keys():
719 raise ValueError ('the event cutflow dictionary already contains an entry ' + selection)
720 else:
721 self._eventcutflow[selection] = decorations
722
723
724 def getEventCutFlow (self, selection) :
725
726 """get the list of decorated selections for an event cutflow, corresponding to
727 key 'selection'
728 """
729 return self._eventcutflow[selection]
730
731
732 def addSelection (self, containerName, selectionName, decoration,
733 **kwargs) :
734 """add another selection decoration to the selection of the given
735 name for the given container"""
736 if selectionName != '' and not self._selectionNameExpr.fullmatch (selectionName) :
737 raise ValueError ('invalid selection name: ' + selectionName)
738 if containerName not in self._containerConfig :
739 self._containerConfig[containerName] = ContainerConfig (containerName, containerName, noSysSuffix=self._noSysSuffix)
740 config = self._containerConfig[containerName]
741 selection = SelectionConfig (selectionName, decoration, **kwargs)
742 config.selections.append (selection)
743
744
745 def addOutputContainer (self, containerName, outputContainerName) :
746 """register a copy of a container used in outputs"""
747 if containerName not in self._containerConfig :
748 raise KeyError ("container unknown: " + containerName)
749 if outputContainerName in self._outputContainers :
750 raise KeyError ("duplicate output container name: " + outputContainerName)
751 self._outputContainers[outputContainerName] = containerName
752
753
754 def checkOutputContainer (self, containerName) :
755 """check whether a given container has been registered in outputs"""
756 return containerName in self._outputContainers.values()
757
758
759 def getOutputContainerOrigin (self, outputContainerName) :
760 """Get the name of the actual container, for which an output is registered"""
761 try:
762 return self._outputContainers[outputContainerName]
763 except KeyError:
764 try:
765 return self._containerConfig[outputContainerName].name
766 except KeyError:
767 raise KeyError ("output container unknown: " + outputContainerName)
768
769
770 def addOutputVar (self, containerName, variableName, outputName,
771 *, noSys=False, enabled=True, auxType=None) :
772 """add an output variable for the given container to the output
773 """
774
775 if containerName not in self._containerConfig :
776 raise KeyError ("container unknown: " + containerName)
777 baseConfig = self._containerConfig[containerName].outputs
778 if outputName in baseConfig :
779 raise KeyError ("duplicate output variable name: " + outputName)
780 config = OutputConfig (containerName, variableName, noSys=noSys, enabled=enabled, auxType=auxType)
781 baseConfig[outputName] = config
782
783
784 def getOutputVars (self, containerName) :
785 """get the output variables for the given container"""
786 if containerName in self._outputContainers :
787 containerName = self._outputContainers[containerName]
788 if containerName not in self._containerConfig :
789 raise KeyError ("unknown container for output: " + containerName)
790 return self._containerConfig[containerName].outputs
791
792
793 def getSelectionNames (self, containerName, excludeFrom = None) :
794 """Retrieve set of unique selections defined for a given container"""
795 if containerName not in self._containerConfig :
796 return []
797 if excludeFrom is None:
798 excludeFrom = set()
799 elif not isinstance(excludeFrom, set) :
800 raise ValueError ('invalid excludeFrom argument (need set of strings): ' + str(excludeFrom))
801
802 config = self._containerConfig[containerName]
803 # because cuts are registered individually, selection names can repeat themselves
804 # but we are interested in unique names only
805 selectionNames = set()
806 for selection in config.selections:
807 if selection.comesFrom in excludeFrom:
808 continue
809 # skip flags which should be disabled in output
810 if selection.writeToOutput:
811 selectionNames.add(selection.name)
812 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)