ATLAS Offline Software
ConfigBlock.py
Go to the documentation of this file.
1 # Copyright (C) 2002-2025 CERN for the benefit of the ATLAS collaboration
2 
3 import textwrap
4 import inspect
5 from functools import wraps
6 
7 from AnaAlgorithm.Logging import logging
8 logCPAlgCfgBlock = logging.getLogger('CPAlgCfgBlock')
9 
10 from AnalysisAlgorithmsConfig.ConfigAccumulator import DataType
11 import re
12 
13 def filter_dsids (filterList, config) :
14  """check whether the sample being run passes a"""
15  """possible DSID filter on the block"""
16  if len(filterList) == 0:
17  return True
18  for dsid_filter in filterList:
19  # Check if the pattern is enclosed in regex delimiters (e.g., starts with '^' or contains regex metacharacters)
20  if any(char in str(dsid_filter) for char in "^$*+?.()|[]{}\\"):
21  pattern = re.compile(dsid_filter)
22  if pattern.match(str(config.dsid())):
23  return True
24  else:
25  # Otherwise it's an exact DSID (but could be int or string)
26  if str(dsid_filter) == str(config.dsid()):
27  return True
28  return False
29 
31  """this wrapper ensures that the 'instanceName' of the various """
32  """config blocks is cleaned up of any non-alphanumeric characters """
33  """that may arise from using 'selectionName' in the naming."""
34  @wraps(func)
35  def wrapper(*args, **kwargs):
36  # Get the string returned by the 'instanceName()' method of a config block
37  orig_name = func(*args, **kwargs)
38 
39  # Allowed replacements - anything else is likely a mistake on the user-side
40  result = orig_name.replace("||", "OR").replace("&&", "AND").replace("(","LB").replace(")","RB").replace(" ","")
41 
42  return result
43  return wrapper
44 
46  """this meta class enforces the application of 'alphanumeric_block_names()' """
47  """to 'instanceName()' and will be used in the main ConfigBlock class in order """
48  """to propagate this rule also to all derived classes (the individual config blocks."""
49  def __new__(cls, name, bases, dct):
50  # Automatically apply alphanumeric-only decorator to 'instanceName()' method
51  if 'instanceName' in dct and callable(dct['instanceName']):
52  dct['instanceName'] = alphanumeric_block_name(dct['instanceName'])
53  return super().__new__(cls, name, bases, dct)
54 
56  """the information for a single option on a configuration block"""
57 
58  def __init__ (self, type=None, info='', noneAction='ignore', required=False,
59  default=None) :
60  self.type = type
61  self.info = info
62  self.required = required
63  self.noneAction = noneAction
64  self.default = default
65 
66 
67 
69  """Class encoding a blocks dependence on other blocks."""
70 
71  def __init__(self, blockName, required=True):
72  self.blockName = blockName
73  self.required = required
74 
75 
76  def __eq__(self, name):
77  return self.blockName == name
78 
79 
80  def __str__(self):
81  return self.blockName
82 
83 
84  def __repr__(self):
85  return f'ConfigBlockDependency(blockName="{self.blockName}", required={self.required})'
86 
87 
88 class ConfigBlock(metaclass=BlockNameProcessorMeta):
89  """the base class for classes implementing individual blocks of
90  configuration
91 
92  A configuration block is a sequence of one or more algorithms that
93  should always be scheduled together, e.g. the muon four momentum
94  corrections could be a single block, muon selection could then be
95  another block. The blocks themselves generally have their own
96  configuration options/properties specific to the block, and will
97  perform a dynamic configuration based on those options as well as
98  the overall job.
99 
100  The actual configuration of the algorithms in the block will
101  depend on what other blocks are scheduled before and afterwards,
102  most importantly some algorithms will introduce shallow copies
103  that subsequent algorithms will need to run on, and some
104  algorithms will add selection decorations that subquent algorithms
105  should use as preselections.
106 
107  The algorithms get created in a multi-step process (that may be
108  extended in the future): As a first step each block retrieves
109  references to the containers it uses (essentially marking its spot
110  in the processing chain) and also registering any shallow copies
111  that will be made. In the second/last step each block then
112  creates the fully configured algorithms.
113 
114  One goal is that when the algorithms get created they will have
115  their final configuration and there needs to be no
116  meta-configuration data attached to the algorithms, essentially an
117  inversion of the approach in AnaAlgSequence in which the
118  algorithms got created first with associated meta-configuration
119  and then get modified in susequent configuration steps.
120 
121  For now this is mostly an empty base class, but another goal of
122  this approach is to make it easier to build another configuration
123  layer on top of this one, and this class will likely be extended
124  and get data members at that point.
125 
126  The child class needs to implement the method `makeAlgs` which is
127  given a single `ConfigAccumulator` type argument. This is meant to
128  create the sequence of algorithms that this block configures. This
129  is currently (28 Jul 2025) called twice and should do the same thing
130  during both calls, but the plan is to change that to a single call.
131 
132  The child class should also implement the method `getInstanceName`
133  which should return a string that is used to distinguish between
134  multiple instances of the same block. This is used to append the
135  instance name to the names of all algorithms created by this block,
136  and may in the future also be used to distinguish between multiple
137  instances of the block.
138  """
139 
140  # Class-level dictionary to keep track of instance counts for each derived class
141  instance_counts = {}
142 
143  def __init__ (self) :
144  self._blockName = ''
145  self._factoryName = None
146  self._dependencies = []
147  self._options = {}
148  # used with block configuration to set arbitrary option
149  self.addOption('groupName', '', type=str,
150  info=('Used to specify this block when setting an'
151  ' option at an arbitrary location.'))
152  self.addOption('skipOnData', False, type=bool,
153  info=('User option to prevent the block from running'
154  ' on data. This only affects blocks that are'
155  ' intended to run on data.'))
156  self.addOption('skipOnMC', False, type=bool,
157  info=('User option to prevent the block from running'
158  ' on MC. This only affects blocks that are'
159  ' intended to run on MC.'))
160  self.addOption('onlyForDSIDs', [], type=list,
161  info=('Used to specify which MC DSIDs to allow this'
162  ' block to run on. Each element of the list'
163  ' can be a full DSID (e.g. 410470), or a regex'
164  ' (e.g. 410.* to select all 410xxx DSIDs, or'
165  ' ^(?!410) to veto them). An empty list means no'
166  ' DSID restriction.'))
167  self.addOption('propertyOverrides', {}, type=None,
168  info=('EXPERT USE ONLY: A dictionary of properties to'
169  ' override at the end of configuration. This should'
170  ' take the form'
171  ' {"algName.toolName.propertyName": value, ...},'
172  ' without any automatically applied postfixes for'
173  ' the algorithm name. THIS IS MEANT TO BE EXPERT'
174  ' USAGE ONLY. Properties that need to be set by'
175  ' the user should be declared as options on the'
176  ' block itself. EXPERT USE ONLY!'))
177  # Increment the instance count for the current class
178  cls = type(self) # Get the actual class of the instance (also derived!)
179  if cls not in ConfigBlock.instance_counts:
180  ConfigBlock.instance_counts[cls] = 0
181  # Note: we do need to check in the call stack that we are
182  # in a real makeConfig situation, and not e.g. printAlgs
183  stack = inspect.stack()
184  for frame_info in stack:
185  # Get the class name (if any) from the frame
186  parent_cls = frame_info.frame.f_locals.get('self', None)
187  if parent_cls is None or not isinstance(parent_cls, ConfigBlock):
188  # If the frame does not belong to an instance of ConfigBlock, it's an external caller
189  if frame_info.function == "makeConfig":
190  ConfigBlock.instance_counts[cls] += 1
191  break
192 
193 
194  def setBlockName(self, name):
195  """Set blockName"""
196  self._blockName = name
197 
198  def getBlockName(self):
199  """Get blockName"""
200  return self._blockName
201 
202  def factoryName(self):
203  """get the factory name for this block
204 
205  This is mostly to give a reliable means of identifying the type
206  of block we have in error messages. This is meant to be
207  automatically set by the factory based on the requested block
208  name, but there are a number of fallbacks. It is best not to
209  assume a specific format, this is mostly meant to be used as an
210  identifier in output messages.
211  """
212  if self._factoryName is not None and self._factoryName != '':
213  return self._factoryName
214  # If no factory name is set and the block has a name, use that
215  if self._blockName is not None and self._blockName != '':
216  return self._blockName
217  # Use the class name as a fallback
218  return self.__class__.__name__
219 
220  def setFactoryName(self, name):
221  """set the factory name for this block
222 
223  This is meant to be called automatically by the factory based on
224  the requested block name. If you are creating a block without a factory,
225  you can call this method to set the factory name manually.
226  """
227  self._factoryName = name
228 
229  def instanceName(self):
230  """Get the name of the instance
231 
232  The name of the instance is used to distinguish between multiple
233  instances of the same block. Most importantly, this will be
234  appended to the names of all algorithms created by this block.
235  This defaults to an empty string, but block implementations
236  should override it with an appropriate name based on identifying
237  options set on this instance. A typical example would be the
238  name of the (main) container, plus potentially the selection or
239  working point.
240 
241  Ideally all blocks should override this method, but for backward
242  compatibility (28 Jul 25) it defaults to an empty string.
243  """
244  return ''
245 
246  def isUsedForConfig(self, config):
247  """
248  whether this block should be used for the given configuration
249 
250  This is used by `ConfigSequence` to determine whether this block
251  should be included in the configuration.
252  """
253  if self.skipOnData and config.dataType() is DataType.Data:
254  return False
255  if self.skipOnMC and config.dataType() is not DataType.Data:
256  return False
257  if not filter_dsids(self.onlyForDSIDs, config):
258  return False
259  return True
260 
261  def applyConfigOverrides(self, config):
262  """
263  Apply any configuration overrides specified in the block's
264  `propertyOverrides` option. This is meant to be called at the
265  end of the configuration process, after all algorithms have been
266  created and configured.
267  """
268  for key, value in self.propertyOverrides.items():
269  # Split the key into algorithm name, tool name, and property name
270  parts = key.split('.')
271  if len(parts) < 2:
272  raise Exception(f"Invalid override key format: {key}")
273  alg = config.getAlgorithm(parts[0])
274  if alg is None:
275  raise Exception(f"Algorithm {parts[0]} not found in config for override: {key}")
276  for name in parts[1:-1]:
277  # Navigate through tools if necessary
278  if hasattr(alg, name):
279  alg = getattr(alg, name)
280  else:
281  raise Exception(f"Tool {name} not found for override: {key}")
282  # Set the property on the algorithm/tool. This is probably a
283  # horrible hack, but `setattr` didn't work for me.
284  alg.__setattr__(parts[-1], value)
285 
286  def addDependency(self, dependencyName, required=True):
287  """
288  Add a dependency for the block. Dependency is corresponds to the
289  blockName of another block. If required is True, will throw an
290  error if dependency is not present; otherwise will move this
291  block after the required block. If required is False, will do
292  nothing if required block is not present; otherwise, it will
293  move block after required block.
294  """
295  if not self.hasDependencies():
296  # add option to block ignore dependencies
297  self.addOption('ignoreDependencies', [], type=list,
298  info='List of dependencies defined in the ConfigBlock to ignore.')
299  self._dependencies.append(ConfigBlockDependency(dependencyName, required))
300 
301  def hasDependencies(self):
302  """Return True if there is a dependency."""
303  return bool(self._dependencies)
304 
305  def getDependencies(self):
306  """Return the list of dependencies. """
307  return self._dependencies
308 
309  def addOption (self, name, defaultValue, *,
310  type, info='', noneAction='ignore', required=False) :
311  """declare the given option on the configuration block
312 
313  This should only be called in the constructor of the
314  configuration block.
315 
316  NOTE: The backend to option handling is slated to be replaced
317  at some point. This particular function should essentially
318  stay the same, but some behavior may change.
319  """
320  if name in self._options :
321  raise KeyError (f'duplicate option: {name}')
322  if type not in [str, bool, int, float, list, None] :
323  raise TypeError (f'unknown option type: {type}')
324  noneActions = ['error', 'set', 'ignore']
325  if noneAction not in noneActions :
326  raise ValueError (f'invalid noneAction: {noneAction} [allowed values: {noneActions}]')
327  setattr (self, name, defaultValue)
328  self._options[name] = ConfigBlockOption(type=type, info=info,
329  noneAction=noneAction, required=required, default=defaultValue)
330 
331 
332  def setOptionValue (self, name, value) :
333  """set the given option on the configuration block
334 
335  NOTE: The backend to option handling is slated to be replaced
336  at some point. This particular function should essentially
337  stay the same, but some behavior may change.
338  """
339 
340  if name not in self._options :
341  raise KeyError (f'unknown option "{name}" in block "{self.__class__.__name__}"')
342  noneAction = self._options[name].noneAction
343  if value is not None or noneAction == 'set' :
344  # check type if specified
345  optType = self._options[name].type
346  # convert int to float to prevent crash
347  if optType is float and type(value) is int:
348  value = float(value)
349  if optType is not None and optType != type(value):
350  raise ValueError(f'{name} for block {self.__class__.__name__} should '
351  f'be of type {optType} not {type(value)}')
352  setattr (self, name, value)
353  elif noneAction == 'ignore' :
354  pass
355  elif noneAction == 'error' :
356  raise ValueError (f'passed None for setting option {name} with noneAction=error')
357 
358 
359  def getOptionValue(self, name):
360  """Returns config option value, if present; otherwise return None"""
361  if name in self._options:
362  return getattr(self, name)
363 
364 
365  def getOptions(self):
366  """Return a copy of the options associated with the block"""
367  return self._options.copy()
368 
369 
370  def printOptions(self, verbose=False, width=60, indent=" "):
371  """
372  Prints options and their values
373  """
374  def printWrap(text, width=60, indent=" "):
375  wrapper = textwrap.TextWrapper(width=width, initial_indent=indent,
376  subsequent_indent=indent)
377  for line in wrapper.wrap(text=text):
378  logCPAlgCfgBlock.info(line)
379 
380  for opt, vals in self.getOptions().items():
381  if verbose:
382  logCPAlgCfgBlock.info(indent + f"\033[4m{opt}\033[0m: {self.getOptionValue(opt)}")
383  logCPAlgCfgBlock.info(indent*2 + f"\033[4mtype\033[0m: {vals.type}")
384  logCPAlgCfgBlock.info(indent*2 + f"\033[4mdefault\033[0m: {vals.default}")
385  logCPAlgCfgBlock.info(indent*2 + f"\033[4mrequired\033[0m: {vals.required}")
386  logCPAlgCfgBlock.info(indent*2 + f"\033[4mnoneAction\033[0m: {vals.noneAction}")
387  printWrap(f"\033[4minfo\033[0m: {vals.info}", indent=indent*2)
388  else:
389  logCPAlgCfgBlock.info(indent + f"{ opt}: {self.getOptionValue(opt)}")
390 
391 
392  def hasOption (self, name) :
393  """whether the configuration block has the given option
394 
395  WARNING: The backend to option handling is slated to be
396  replaced at some point. This particular function may change
397  behavior, interface or be removed/replaced entirely.
398  """
399  return name in self._options
400 
401 
402  def __eq__(self, blockName):
403  """
404  Implementation of == operator. Used for seaching configSeque.
405  E.g. if blockName in configSeq:
406  """
407  return self._blockName == blockName
408 
409 
410  def __str__(self):
411  return self._blockName
412 
413 
414  @classmethod
416  # Access the current count for this class
417  return ConfigBlock.instance_counts.get(cls, 0)
python.ConfigBlock.ConfigBlock.hasOption
def hasOption(self, name)
Definition: ConfigBlock.py:392
replace
std::string replace(std::string s, const std::string &s2, const std::string &s3)
Definition: hcg.cxx:307
python.ConfigBlock.ConfigBlock.instanceName
def instanceName(self)
Definition: ConfigBlock.py:229
python.ConfigBlock.ConfigBlockDependency
Definition: ConfigBlock.py:68
python.ConfigBlock.ConfigBlockOption.noneAction
noneAction
Definition: ConfigBlock.py:62
python.ConfigBlock.ConfigBlock.hasDependencies
def hasDependencies(self)
Definition: ConfigBlock.py:301
python.ConfigBlock.ConfigBlock.addDependency
def addDependency(self, dependencyName, required=True)
Definition: ConfigBlock.py:286
python.ConfigBlock.ConfigBlock.applyConfigOverrides
def applyConfigOverrides(self, config)
Definition: ConfigBlock.py:261
python.ConfigBlock.ConfigBlock.__str__
def __str__(self)
Definition: ConfigBlock.py:410
python.ConfigBlock.BlockNameProcessorMeta
Definition: ConfigBlock.py:45
python.ConfigBlock.ConfigBlock.setOptionValue
def setOptionValue(self, name, value)
Definition: ConfigBlock.py:332
python.ConfigBlock.ConfigBlockOption.type
type
Definition: ConfigBlock.py:59
python.ConfigBlock.ConfigBlock._factoryName
_factoryName
Definition: ConfigBlock.py:145
python.ConfigBlock.ConfigBlock.getDependencies
def getDependencies(self)
Definition: ConfigBlock.py:305
python.ConfigBlock.ConfigBlockOption.info
info
Definition: ConfigBlock.py:60
python.ConfigBlock.alphanumeric_block_name
def alphanumeric_block_name(func)
Definition: ConfigBlock.py:30
python.ConfigBlock.ConfigBlock.getOptions
def getOptions(self)
Definition: ConfigBlock.py:365
dumpHVPathFromNtuple.append
bool append
Definition: dumpHVPathFromNtuple.py:91
python.ConfigBlock.ConfigBlockDependency.__str__
def __str__(self)
Definition: ConfigBlock.py:80
python.CaloAddPedShiftConfig.type
type
Definition: CaloAddPedShiftConfig.py:42
python.ConfigBlock.ConfigBlockOption.required
required
Definition: ConfigBlock.py:61
python.ConfigBlock.ConfigBlock.setFactoryName
def setFactoryName(self, name)
Definition: ConfigBlock.py:220
python.ConfigBlock.ConfigBlock.isUsedForConfig
def isUsedForConfig(self, config)
Definition: ConfigBlock.py:246
python.ConfigBlock.ConfigBlockOption.default
default
Definition: ConfigBlock.py:63
python.ConfigBlock.ConfigBlockDependency.__eq__
def __eq__(self, name)
Definition: ConfigBlock.py:76
python.ConfigBlock.ConfigBlock._dependencies
_dependencies
Definition: ConfigBlock.py:146
python.ConfigBlock.ConfigBlockDependency.blockName
blockName
Definition: ConfigBlock.py:72
python.ConfigBlock.ConfigBlock.get_instance_count
def get_instance_count(cls)
Definition: ConfigBlock.py:415
python.ConfigBlock.ConfigBlock.getBlockName
def getBlockName(self)
Definition: ConfigBlock.py:198
python.ConfigBlock.ConfigBlockDependency.required
required
Definition: ConfigBlock.py:73
python.ConfigBlock.ConfigBlock.__init__
def __init__(self)
Definition: ConfigBlock.py:143
python.ConfigBlock.BlockNameProcessorMeta.__new__
def __new__(cls, name, bases, dct)
Definition: ConfigBlock.py:49
python.ConfigBlock.ConfigBlock.printOptions
def printOptions(self, verbose=False, width=60, indent=" ")
Definition: ConfigBlock.py:370
python.ConfigBlock.ConfigBlock.addOption
def addOption(self, name, defaultValue, *type, info='', noneAction='ignore', required=False)
Definition: ConfigBlock.py:309
python.ConfigBlock.ConfigBlock.getOptionValue
def getOptionValue(self, name)
Definition: ConfigBlock.py:359
TrigJetMonitorAlgorithm.items
items
Definition: TrigJetMonitorAlgorithm.py:71
python.ConfigBlock.ConfigBlock.__eq__
def __eq__(self, blockName)
Definition: ConfigBlock.py:402
python.ConfigBlock.ConfigBlock._blockName
_blockName
Definition: ConfigBlock.py:144
python.ConfigBlock.filter_dsids
def filter_dsids(filterList, config)
Definition: ConfigBlock.py:13
python.ConfigBlock.ConfigBlock.factoryName
def factoryName(self)
Definition: ConfigBlock.py:202
python.ConfigBlock.ConfigBlockDependency.__init__
def __init__(self, blockName, required=True)
Definition: ConfigBlock.py:71
python.ConfigBlock.ConfigBlockDependency.__repr__
def __repr__(self)
Definition: ConfigBlock.py:84
str
Definition: BTagTrackIpAccessor.cxx:11
calibdata.copy
bool copy
Definition: calibdata.py:26
python.ConfigBlock.ConfigBlock._options
_options
Definition: ConfigBlock.py:147
python.ConfigBlock.ConfigBlockOption.__init__
def __init__(self, type=None, info='', noneAction='ignore', required=False, default=None)
Definition: ConfigBlock.py:58
xAOD::bool
setBGCode setTAP setLVL2ErrorBits bool
Definition: TrigDecision_v1.cxx:60
python.ConfigBlock.ConfigBlock
Definition: ConfigBlock.py:88
python.ConfigBlock.ConfigBlockOption
Definition: ConfigBlock.py:55
python.ConfigBlock.ConfigBlock.setBlockName
def setBlockName(self, name)
Definition: ConfigBlock.py:194
python.LArMinBiasAlgConfig.float
float
Definition: LArMinBiasAlgConfig.py:65