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