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