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)
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.
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."""
604 for containerConfig in self._containerConfig.values() :
605 if not containerConfig.names :
607 base = containerConfig.name
608 lastName = containerConfig.names[-1]
609 match = re.match (re.escape (base) + r'_STEP\d+', lastName)
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 :
617 for component in self._algorithms.values() :
618 substituteComponentProperties (component, substitutions)
713 def getFullSelection (self, containerName, selectionName,
714 *, skipBase = False, excludeFrom = None) :
716 """get the selection string for the given selection on the given
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.
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
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 :
738 if excludeFrom is None :
740 elif not isinstance(excludeFrom, set) :
741 raise ValueError ('invalid excludeFrom argument (need set of strings): ' + str(excludeFrom))
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) :
751 while selectionName != '' :
752 match = self._selectionNameExpr.match (selectionName)
754 result += selectionName[0]
755 selectionName = selectionName[1:]
757 subname = match.group(0)
758 subresult = self.getFullSelection (containerName, subname, skipBase = True, excludeFrom=excludeFrom)
760 result += '(' + subresult + ')'
763 selectionName = selectionName[len(subname):]
764 subresult = self.getFullSelection (containerName, '', excludeFrom=excludeFrom)
766 result = subresult + '&&(' + result + ')'
767 return '(' + result + ')' if result !='' else ''
769 config = self._containerConfig[containerName]
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)
782 def getSelectionCutFlow (self, containerName, selectionName) :
784 """get the individual selections as a list for producing the cutflow for
785 the given selection on the given container
787 This can only handle individual selections, not selection
788 expressions (e.g. `loose||tight`).
791 if containerName not in self._containerConfig :
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)
803 config = self._containerConfig[containerName]
805 for selection in config.selections :
806 if (selection.name == '' or selection.name == selectionName) :
807 decorations += [selection.decoration]