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):
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)
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")
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")
248 raise ValueError("Cannot pass both dataYear and flags/autoconfigFromFlags arguments")
250 if self._flags.Input.isMC:
251 if self._flags.Sim.ISF.Simulator.usesFastCaloSim():
252 dataType = DataType.FastSim
254 dataType = DataType.FullSim
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)
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()
266 raise ValueError ("need to specify dataType if flags are not set")
267 # legacy mappings of string arguments
268 if isinstance(dataType, str):
270 dataType = DataType.FullSim
271 elif dataType == 'afii':
272 dataType = DataType.FastSim
274 dataType = DataType(dataType)
275 if isPhyslite is None:
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
284 flags.Input.MCChannelNumber = dsid
285 if campaign is not None:
286 flags.Input.MCCampaign = campaign
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
293 flags.Input.RunNumbers = [runNumber]
294 hltSummary = 'HLTNav_Summary_DAODSlimmed'
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
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 = {}
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)
331 raise ValueError ("need to pass algSeq if not using ComponentAccumulator")
333 ConfigAccumulator._instance_counter += 1
334 self._algPrefix = f'seq{self._instance_counter}_'
458 def createService (self, type, name, isSingleton=True) :
459 '''create a new service and register it as the "current algorithm"'''
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
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)
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
480 ConfigAccumulator._singleton_registry[name] = service
484 def createPublicTool (self, type, name, isSingleton=True) :
485 '''create a new public tool and register it as the "current algorithm"'''
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
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)
501 # We're not, so let's remember this as a "normal" algorithm:
503 self._algorithms[name] = tool
504 self._currentAlg = tool
506 ConfigAccumulator._singleton_registry[name] = tool
524 def setSourceName (self, containerName, sourceName,
525 *, originalName = None, isMet = False) :
526 """set the (default) name of the source/original container
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
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
539 if containerName not in self._containerConfig :
540 self._containerConfig[containerName] = ContainerConfig (containerName, sourceName, noSysSuffix = self._noSysSuffix, originalName = originalName, isMet = isMet)
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.
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."""
607 for containerConfig in self._containerConfig.values() :
608 if not containerConfig.names :
610 base = containerConfig.name
611 lastName = containerConfig.names[-1]
612 match = re.match (re.escape (base) + r'_STEP\d+', lastName)
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 :
620 for component in self._algorithms.values() :
621 substituteComponentProperties (component, substitutions)
716 def getFullSelection (self, containerName, selectionName,
717 *, skipBase = False, excludeFrom = None) :
719 """get the selection string for the given selection on the given
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.
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
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 :
741 if excludeFrom is None :
743 elif not isinstance(excludeFrom, set) :
744 raise ValueError ('invalid excludeFrom argument (need set of strings): ' + str(excludeFrom))
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) :
754 while selectionName != '' :
755 match = self._selectionNameExpr.match (selectionName)
757 result += selectionName[0]
758 selectionName = selectionName[1:]
760 subname = match.group(0)
761 subresult = self.getFullSelection (containerName, subname, skipBase = True, excludeFrom=excludeFrom)
763 result += '(' + subresult + ')'
766 selectionName = selectionName[len(subname):]
767 subresult = self.getFullSelection (containerName, '', excludeFrom=excludeFrom)
769 result = subresult + '&&(' + result + ')'
770 return '(' + result + ')' if result !='' else ''
772 config = self._containerConfig[containerName]
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)
785 def getSelectionCutFlow (self, containerName, selectionName) :
787 """get the individual selections as a list for producing the cutflow for
788 the given selection on the given container
790 This can only handle individual selections, not selection
791 expressions (e.g. `loose||tight`).
794 if containerName not in self._containerConfig :
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)
806 config = self._containerConfig[containerName]
808 for selection in config.selections :
809 if (selection.name == '' or selection.name == selectionName) :
810 decorations += [selection.decoration]
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
870 if containerName in self._outputContainers:
871 self.addOutputVar(self.getOutputContainerOrigin(containerName), variableName, outputName, noSys=noSys, enabled=enabled, auxType=auxType)
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