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 in self._outputContainers:
566 return f"{containerName}_%SYS%"
567
568 if containerName not in self._containerConfig :
569 raise Exception ("no source container for: " + containerName)
570 return self._containerConfig[containerName].currentName(nominal=nominal)
571
572
573 def copyName (self, containerName) :
574 """register that a copy of the container will be made and return
575 its name"""
576 if containerName not in self._containerConfig :
577 raise Exception ("unknown container: " + containerName)
578 return self._containerConfig[containerName].appendStep()
579
580
581 def wantCopy (self, containerName) :
582 """ask whether we want/need a copy of the container
583
584 This usually only happens if no copy of the container has been
585 made yet and the copy is needed to allow modifications, etc.
586 """
587 if containerName not in self._containerConfig :
588 raise Exception ("no source container for: " + containerName)
589 config = self._containerConfig[containerName]
590 if len (config.names) == 0 :
591 raise Exception ("checking wantCopy on container with no name in event store: " + containerName)
592 return config.names[-1] == config.sourceName
593
594
595 def renameFinalContainers (self) :
596 """post-process the configured algorithms, tools and services to
597 strip the auto-generated `_STEP<n>` suffix from each container's
598 *final* name in every property value.
599
600 This is mostly needed in case the user has further downstream
601 algorithms that rely on the exact name of containers in the
602 event store. For anything configured through the
603 `ConfigAccumulator` this doesn't matter, as the names are
604 configured consistently."""
605
606 substitutions = []
607 for containerConfig in self._containerConfig.values() :
608 if not containerConfig.names :
609 continue
610 base = containerConfig.name
611 lastName = containerConfig.names[-1]
612 match = re.match (re.escape (base) + r'_STEP\d+', lastName)
613 if match :
614 substitutions.append ((match.group(0), base))
615 # keep ContainerConfig in sync, in case anything reads
616 # currentName() after this pass
617 containerConfig.names[-1] = substituteValue (lastName, [(match.group(0), base)])
618 if not substitutions :
619 return
620 for component in self._algorithms.values() :
621 substituteComponentProperties (component, substitutions)
622
623
624 def originalName (self, containerName) :
625 """get the "original" name of the given container
626
627 This is mostly/exclusively used for jet containers, so that
628 subsequent configurations know which jet container they
629 operate on.
630 """
631 if containerName not in self._containerConfig :
632 raise Exception ("container unknown: " + containerName)
633 result = self._containerConfig[containerName].originalName
634 if result is None :
635 raise Exception ("no original name for: " + containerName)
636 return result
637
638 def getContainerMeta (self, containerName, metaField, defaultValue=None, *, failOnMiss=False) :
639 """get the meta information for the given container
640
641 This is used to pass down meta-information from the
642 configuration to the algorithms.
643 """
644 if containerName not in self._containerConfig :
645 raise Exception ("container unknown: " + containerName)
646 if metaField in self._containerConfig[containerName].meta :
647 return self._containerConfig[containerName].meta[metaField]
648 if failOnMiss :
649 raise Exception ('unknown meta-field' + metaField + ' on container ' + containerName)
650 return defaultValue
651
652 def setContainerMeta (self, containerName, metaField, value, *, allowOverwrite=False) :
653 """set the meta information for the given container
654
655 This is used to pass down meta-information from the
656 configuration to the algorithms.
657 """
658 if containerName not in self._containerConfig :
659 raise Exception ("container unknown: " + containerName)
660 if not allowOverwrite and metaField in self._containerConfig[containerName].meta :
661 raise Exception ('duplicate meta-field' + metaField + ' on container ' + containerName)
662 self._containerConfig[containerName].meta[metaField] = value
663
664 def isMetContainer (self, containerName) :
665 """whether the given container is registered as a MET container
666
667 This is mostly/exclusively used for determining whether to
668 write out the whole container or just a single MET term.
669 """
670 if containerName not in self._containerConfig :
671 raise Exception ("container unknown: " + containerName)
672 return self._containerConfig[containerName].isMet
673
674
675 def readNameAndSelection (self, containerName, *, excludeFrom = None) :
676 """get the name of the "current copy" of the given container, and the
677 selection string
678
679 This is mostly meant for MET and OR for whom the actual object
680 selection is relevant, and which as such allow to pass in the
681 working point as "ObjectName.WorkingPoint".
682 """
683 split = containerName.split (".")
684 if len(split) == 1 :
685 objectName = split[0]
686 selectionName = ''
687 elif len(split) == 2 :
688 objectName = split[0]
689 selectionName = split[1]
690 else :
691 raise Exception ('invalid object selection name: ' + containerName)
692 return self.readName (objectName), self.getFullSelection (objectName, selectionName, excludeFrom=excludeFrom)
693
694
695 def getPreselection (self, containerName, selectionName, *, asList = False) :
696
697 """get the preselection string for the given selection on the given
698 container
699 """
700 if selectionName != '' and not self._selectionNameExpr.fullmatch (selectionName) :
701 raise ValueError ('invalid selection name: ' + selectionName)
702 if containerName not in self._containerConfig :
703 return ""
704 config = self._containerConfig[containerName]
705 decorations = []
706 for selection in config.selections :
707 if (selection.name == '' or selection.name == selectionName) and \
708 selection.preselection :
709 decorations += [selection.decoration]
710 if asList :
711 return decorations
712 else :
713 return '&&'.join (decorations)
714
715
716 def getFullSelection (self, containerName, selectionName,
717 *, skipBase = False, excludeFrom = None) :
718
719 """get the selection string for the given selection on the given
720 container
721
722 This can handle both individual selections or selection
723 expressions (e.g. `loose||tight`) with the later being
724 properly expanded. Either way the base selection (i.e. the
725 selection without a name) will always be applied on top.
726
727 containerName --- the container the selection is defined on
728 selectionName --- the name of the selection, or a selection
729 expression based on multiple named selections
730 skipBase --- will avoid the base selection, and should normally
731 not be used by the end-user.
732 excludeFrom --- a set of string names of selection sources to exclude
733 e.g. to exclude OR selections from MET
734 """
735 if "." in containerName:
736 raise ValueError (f'invalid containerName argument: {containerName} , it contains a "." '
737 'which is used to indicate container+selection. You should only pass the container.')
738 if containerName not in self._containerConfig :
739 return ""
740
741 if excludeFrom is None :
742 excludeFrom = set()
743 elif not isinstance(excludeFrom, set) :
744 raise ValueError ('invalid excludeFrom argument (need set of strings): ' + str(excludeFrom))
745
746 # Check if this is actually a selection expression,
747 # e.g. `A||B` and if so translate it into a complex expression
748 # for the user. I'm not trying to do any complex syntax
749 # recognition, but instead just produce an expression that the
750 # C++ parser ought to be able to read.
751 if selectionName != '' and \
752 not self._selectionNameExpr.fullmatch (selectionName) :
753 result = ''
754 while selectionName != '' :
755 match = self._selectionNameExpr.match (selectionName)
756 if not match :
757 result += selectionName[0]
758 selectionName = selectionName[1:]
759 else :
760 subname = match.group(0)
761 subresult = self.getFullSelection (containerName, subname, skipBase = True, excludeFrom=excludeFrom)
762 if subresult != '' :
763 result += '(' + subresult + ')'
764 else :
765 result += 'true'
766 selectionName = selectionName[len(subname):]
767 subresult = self.getFullSelection (containerName, '', excludeFrom=excludeFrom)
768 if subresult != '' :
769 result = subresult + '&&(' + result + ')'
770 return '(' + result + ')' if result !='' else ''
771
772 config = self._containerConfig[containerName]
773 decorations = []
774 hasSelectionName = False
775 for selection in config.selections :
776 if ((selection.name == '' and not skipBase) or selection.name == selectionName) and (selection.comesFrom not in excludeFrom) :
777 decorations += [selection.decoration]
778 if selection.name == selectionName :
779 hasSelectionName = True
780 if not hasSelectionName and selectionName != '' :
781 raise KeyError ('invalid selection name: ' + containerName + '.' + selectionName)
782 return '&&'.join (decorations)
783
784
785 def getSelectionCutFlow (self, containerName, selectionName) :
786
787 """get the individual selections as a list for producing the cutflow for
788 the given selection on the given container
789
790 This can only handle individual selections, not selection
791 expressions (e.g. `loose||tight`).
792
793 """
794 if containerName not in self._containerConfig :
795 return []
796
797 # Check if this is actually a selection expression,
798 # e.g. `A||B` and if so translate it into a complex expression
799 # for the user. I'm not trying to do any complex syntax
800 # recognition, but instead just produce an expression that the
801 # C++ parser ought to be able to read.
802 if selectionName != '' and \
803 not self._selectionNameExpr.fullmatch (selectionName) :
804 raise ValueError ('not allowed to do cutflow on selection expression: ' + selectionName)
805
806 config = self._containerConfig[containerName]
807 decorations = []
808 for selection in config.selections :
809 if (selection.name == '' or selection.name == selectionName) :
810 decorations += [selection.decoration]
811 return decorations
812
813
814 def addEventCutFlow (self, selection, decorations) :
815
816 """register a new event cutflow, adding it to the dictionary with key 'selection'
817 and value 'decorations', a list of decorated selections
818 """
819 if selection in self._eventcutflow.keys():
820 raise ValueError ('the event cutflow dictionary already contains an entry ' + selection)
821 else:
822 self._eventcutflow[selection] = decorations
823
824
825 def getEventCutFlow (self, selection) :
826
827 """get the list of decorated selections for an event cutflow, corresponding to
828 key 'selection'
829 """
830 return self._eventcutflow[selection]
831
832
833 def addSelection (self, containerName, selectionName, decoration,
834 **kwargs) :
835 """add another selection decoration to the selection of the given
836 name for the given container"""
837 if selectionName != '' and not self._selectionNameExpr.fullmatch (selectionName) :
838 raise ValueError ('invalid selection name: ' + selectionName)
839 if containerName not in self._containerConfig :
840 self._containerConfig[containerName] = ContainerConfig (containerName, containerName, noSysSuffix=self._noSysSuffix)
841 config = self._containerConfig[containerName]
842 selection = SelectionConfig (selectionName, decoration, **kwargs)
843 config.selections.append (selection)
844
845
846 def addOutputContainer (self, containerName, outputContainerName) :
847 """register a copy of a container used in outputs"""
848 if containerName not in self._containerConfig :
849 raise KeyError ("container unknown: " + containerName)
850 if outputContainerName in self._outputContainers :
851 raise KeyError ("duplicate output container name: " + outputContainerName)
852 self._outputContainers[outputContainerName] = containerName
853
854
855 def getOutputContainerOrigin (self, outputContainerName) :
856 """Get the name of the actual container, for which an output is registered"""
857 try:
858 return self._outputContainers[outputContainerName]
859 except KeyError:
860 try:
861 return self._containerConfig[outputContainerName].name
862 except KeyError:
863 raise KeyError ("output container unknown: " + outputContainerName)
864
865
866 def addOutputVar (self, containerName, variableName, outputName,
867 *, noSys=False, enabled=True, auxType=None) :
868 """add an output variable for the given container to the output
869 """
870 if containerName in self._outputContainers:
871 self.addOutputVar(self.getOutputContainerOrigin(containerName), variableName, outputName, noSys=noSys, enabled=enabled, auxType=auxType)
872 return
873
874 if containerName not in self._containerConfig :
875 raise KeyError ("container unknown: " + containerName)
876 baseConfig = self._containerConfig[containerName].outputs
877 if outputName in baseConfig :
878 raise KeyError ("duplicate output variable name: " + outputName)
879 config = OutputConfig (containerName, variableName, noSys=noSys, enabled=enabled, auxType=auxType)
880 baseConfig[outputName] = config
881
882
883 def getOutputVars (self, containerName) :
884 """get the output variables for the given container"""
885 if containerName in self._outputContainers :
886 containerName = self._outputContainers[containerName]
887 if containerName not in self._containerConfig :
888 raise KeyError ("unknown container for output: " + containerName)
889 return self._containerConfig[containerName].outputs
890
891
892 def getSelectionNames (self, containerName, excludeFrom = None) :
893 """Retrieve set of unique selections defined for a given container"""
894 if containerName not in self._containerConfig :
895 return []
896 if excludeFrom is None:
897 excludeFrom = set()
898 elif not isinstance(excludeFrom, set) :
899 raise ValueError ('invalid excludeFrom argument (need set of strings): ' + str(excludeFrom))
900
901 config = self._containerConfig[containerName]
902 # because cuts are registered individually, selection names can repeat themselves
903 # but we are interested in unique names only
904 selectionNames = set()
905 for selection in config.selections:
906 if selection.comesFrom in excludeFrom:
907 continue
908 # skip flags which should be disabled in output
909 if selection.writeToOutput:
910 selectionNames.add(selection.name)
911 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)