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