ATLAS Offline Software
ConfigText.py
Go to the documentation of this file.
1 # Copyright (C) 2002-2024 CERN for the benefit of the ATLAS collaboration
2 #
3 # @author Joseph Lambert
4 
5 import yaml
6 import json
7 import os
8 import sys
9 import importlib
10 import pathlib
11 import warnings
12 
13 from AnalysisAlgorithmsConfig.ConfigSequence import ConfigSequence
14 from AnalysisAlgorithmsConfig.ConfigFactory import ConfigFactory
15 from AnalysisAlgorithmsConfig.ConfigAccumulator import deprecationWarningCategory
16 
17 from AnaAlgorithm.Logging import logging
18 logCPAlgTextCfg = logging.getLogger('CPAlgTextCfg')
19 
20 
21 def readYaml(yamlPath):
22  """Loads YAML file into a dictionary"""
23  if not os.path.isfile(yamlPath):
24  raise ValueError(f"{yamlPath} is not a file.")
25  with open(yamlPath, 'r') as f:
26  textConfig = yaml.safe_load(f)
27  return textConfig
28 
29 
30 def printYaml(d, sort=False, jsonFormat=False):
31  """Prints a dictionary as YAML"""
32  print(yaml.dump(d, default_flow_style=jsonFormat, sort_keys=sort))
33 
34 
35 class TextConfig(ConfigFactory):
36  def __init__(self, yamlPath=None, *, config=None, addDefaultBlocks=True):
37  super().__init__(addDefaultBlocks=False)
38 
39  if yamlPath and config:
40  raise ValueError("Cannot specify both yamlPath and config. Use one or the other.")
41 
42  # Block to add new blocks to this object
43  self.addAlgConfigBlock(algName="AddConfigBlocks", alg=self._addNewConfigBlocks,
44  defaults={'self': self})
45  # add default blocks
46  if addDefaultBlocks:
47  self.addDefaultAlgs()
48  # load yaml
49  self._config = {}
50  # do not allow for loading multiple yaml files
51  self.__loadedYaml = False
52  if yamlPath is not None or config is not None:
53  self.loadConfig(yamlPath, configDict=config)
54  # last is used for setOptionValue when using addBlock
55  self._last = None
56 
57 
58  def setConfig(self, config):
59  """Print YAML configuration file."""
60  if self._config:
61  raise ValueError("Configuration has already been loaded.")
62  self._config = config
63  return
64 
65  # Less-than-ideal fix introduced in !76767
66  def preprocessConfig(self, config, algs):
67  """
68  Preprocess the configuration dictionary.
69  Ensure blocks with only sub-blocks are initialized with an empty dictionary.
70  """
71  def processNode(node, algs):
72  if not isinstance(node, dict):
73  return # Base case: not a dictionary
74  for blockName, blockContent in list(node.items()):
75  # If the block name is recognized in algs
76  if blockName in algs:
77  # If the block only defines sub-blocks, initialize it
78  if isinstance(blockContent, dict) and not any(
79  key in algs[blockName].options for key in blockContent
80  ):
81  # Ensure parent block is initialized as an empty dictionary
82  node[blockName] = {'__placeholder__': True, **blockContent}
83  # Recurse into sub-blocks
84  processNode(node[blockName], algs[blockName].subAlgs)
85 
86  # Start processing from the root of the configuration
87  processNode(config, algs)
88 
89  # Less-than-ideal fix introduced in !76767
90  def cleanupPlaceholders(self, config):
91  """
92  Remove placeholder markers after initialization.
93  """
94  if not isinstance(config, dict):
95  return
96  if "__placeholder__" in config:
97  del config["__placeholder__"]
98  for key, value in config.items():
99  self.cleanupPlaceholders(value)
100 
101  def loadConfig(self, yamlPath=None, *, configDict=None):
102  """
103  read a YAML file. Will combine with any config blocks added using python
104  """
105  if self.__loadedYaml or isinstance(yamlPath, list):
106  raise NotImplementedError("Mering multiple yaml files is not implemented.")
107  self.__loadedYaml = True
108 
109  def merge(config, algs, path=''):
110  """Add to config block-by-block"""
111  if not isinstance(config, list):
112  config = [config]
113  # loop over list of blocks with same block name
114  for blocks in config:
115  # deal with case where empty dict is config
116  if blocks == {} and path:
117  self.addBlock(path)
118  return
119  # remove any subBlocks from block config
120  subBlocks = {}
121  for blockName in algs:
122  if blockName in blocks:
123  subBlocks[blockName] = blocks.pop(blockName)
124  # anything left should be a block and it's configuration
125  if blocks:
126  self.addBlock(path, **blocks)
127  # add in any subBlocks
128  for subName, subBlock in subBlocks.items():
129  newPath = f'{path}.{subName}' if path else subName
130  merge(subBlock, algs[subName].subAlgs, newPath)
131  return
132 
133  logCPAlgTextCfg.info(f'loading {yamlPath}')
134  if configDict is not None:
135  # if configDict is provided, use it directly
136  config = configDict
137  else:
138  config = readYaml(yamlPath)
139  # check if blocks are defined in yaml file
140  if "AddConfigBlocks" in config:
141  self._configureAlg(self._algs["AddConfigBlocks"], config["AddConfigBlocks"])
142 
143  # Preprocess the configuration dictionary (see !76767)
144  self.preprocessConfig(config, self._algs)
145 
146  merge(config, self._algs)
147 
148  # Cleanup placeholders (see !76767)
149  self.cleanupPlaceholders(config)
150 
151  return
152 
153 
154  def printConfig(self, sort=False, jsonFormat=False):
155  """Print YAML configuration file."""
156  if self._config is None:
157  raise ValueError("No configuration has been loaded.")
158  printYaml(self._config, sort, jsonFormat)
159  return
160 
161 
162  def saveYaml(self, filePath='config.yaml', default_flow_style=False,
163  **kwargs):
164  """
165  Convert dictionary representation to yaml and save
166  """
167  logCPAlgTextCfg.info(f"Saving configuration to {filePath}")
168  config = self._config
169  with open(filePath, 'w') as outfile:
170  yaml.dump(config, outfile, default_flow_style=False, **kwargs)
171  return
172 
173 
174  def addBlock(self, name, **kwargs):
175  """
176  Create entry into dictionary representing the text configuration
177  """
178  def setEntry(name, config, opts):
179  if '.' not in name:
180  if name not in config:
181  config[name] = opts
182  elif isinstance(config[name], list):
183  config[name].append(opts)
184  else:
185  config[name] = [config[name], opts]
186  # set last added block for setOptionValue
187  self._last = opts
188  else:
189  name, rest = name[:name.index('.')], name[name.index('.') + 1:]
190  config = config[name]
191  if isinstance(config, list):
192  config = config[-1]
193  setEntry(rest, config, opts)
194  return
195  setEntry(name, self._config, dict(kwargs))
196  return
197 
198 
199  def setOptions(self, **kwargs):
200  """
201  Set option(s) for the lsat block that was added. If an option
202  was added previously, will update value
203  """
204  if self._last is None:
205  raise TypeError("Cannot set options before adding a block")
206  # points to dict with opts for last added block
207  self._last.update(**kwargs)
208 
209 
210  def configure(self):
211  """Process YAML configuration file and confgure added algorithms."""
212  # make sure all blocks in yaml file are added (otherwise they would be ignored)
213  for blockName in self._config:
214  if blockName not in self._order[self.ROOTNAME]:
215  if not blockName:
216  blockName = list(self._config[blockName].keys())[0]
217  raise ValueError(f"Unkown block {blockName} in yaml file")
218 
219  # configure blocks
220  configSeq = ConfigSequence()
221  for blockName in self._order[self.ROOTNAME]:
222  if blockName == "AddConfigBlocks":
223  continue
224 
225  assert blockName in self._algs
226 
227  # order only applies to root blocks
228  if blockName in self._config:
229  blockConfig = self._config[blockName]
230  alg = self._algs[blockName]
231  self._configureAlg(alg, blockConfig, configSeq)
232  else:
233  continue
234  return configSeq
235 
236 
237  def _addNewConfigBlocks(self, modulePath, functionName,
238  algName, defaults=None, pos=None, superBlocks=None):
239  """
240  Load <functionName> from <modulePath>
241  """
242  try:
243  module = importlib.import_module(modulePath)
244  fxn = getattr(module, functionName)
245  except ModuleNotFoundError as e:
246  raise ModuleNotFoundError(f"{e}\nFailed to load {functionName} from {modulePath}")
247  else:
248  sys.modules[functionName] = fxn
249  # add new algorithm to available algorithms
250  self.addAlgConfigBlock(algName=algName, alg=fxn,
251  defaults=defaults,
252  superBlocks=superBlocks,
253  pos=pos)
254  return
255 
256 
257  def _configureAlg(self, block, blockConfig, configSeq=None, containerName=None,
258  extraOptions=None):
259  if not isinstance(blockConfig, list):
260  blockConfig = [blockConfig]
261 
262  for options in blockConfig:
263  # Special case: propogate containerName down to subAlgs
264  if 'containerName' in options:
265  containerName = options['containerName']
266  elif containerName is not None and 'containerName' not in options:
267  options['containerName'] = containerName
268  # will check which options are associated alg and not options
269  logCPAlgTextCfg.info(f"Configuring {block.algName}")
270  seq, funcOpts = block.makeConfig(options)
271  if not seq._blocks:
272  continue
273  algOpts = seq.setOptions(options)
274  # If containerName was not set explicitly, we can now retrieve
275  # its default value
276  if containerName is None:
277  for opt in algOpts:
278  if 'name' in opt and opt['name'] == 'containerName':
279  containerName = opt.get('value', None)
280  break # Exit the loop as we've found the key
281 
282  if configSeq is not None:
283  configSeq += seq
284 
285  # propagate special extra options to subalgs
286  if extraOptions is None:
287  extraOptionsList = ["skipOnData", "skipOnMC", "onlyForDSIDs"]
288  for i in algOpts:
289  if i['name'] in extraOptionsList and i['defaultValue'] != i['value']:
290  if extraOptions is None:
291  extraOptions = {}
292  extraOptions[i['name']] = i['value']
293  else:
294  algOpts = seq.setOptions(extraOptions.copy())
295 
296  # check to see if there are unused parameters
297  algOpts = [i['name'] for i in algOpts]
298  expectedOptions = set(funcOpts)
299  expectedOptions |= set(algOpts)
300  expectedOptions |= set(block.subAlgs)
301 
302  difference = set(options.keys()) - expectedOptions
303  difference.discard('__placeholder__')
304  if difference:
305  difference = "\n".join(difference)
306  raise ValueError(f"There are options set that are not used for "
307  f"{block.algName}:\n{difference}\n"
308  "Please check your configuration.")
309 
310  # check for sub-blocks and call this function recursively
311  for alg in self._order.get(block.algName, []):
312  if alg in options:
313  subAlg = block.subAlgs[alg]
314  self._configureAlg(subAlg, options[alg], configSeq, containerName, extraOptions)
315  return configSeq
316 
317 
318 def makeSequence(configPath, *, flags=None, algSeq=None, noSystematics=None, dataType=None, geometry=None, autoconfigFromFlags=None, isPhyslite=None, noPhysliteBroken=False):
319  """
320  """
321 
322  # Historically we have used the identifier
323  # `autoconfigFromFlags`, but in the rest of the code base
324  # `flags` is used. So for now we allow either, and can hopefully
325  # at some point remove the former (21 Aug 25).
326  if autoconfigFromFlags is not None:
327  if flags is not None:
328  raise ValueError("Cannot pass both flags and autoconfigFromFlags arguments")
329  flags = autoconfigFromFlags
330  warnings.warn ('Using autoconfigFromFlags parameter is deprecated, use flags instead', category=deprecationWarningCategory, stacklevel=2)
331  elif flags is None:
332  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)
333 
334  from AnalysisAlgorithmsConfig.ConfigAccumulator import ConfigAccumulator
335 
336  config = TextConfig(configPath)
337 
338  logCPAlgTextCfg.info("Configuration file read in:")
339  config.printConfig()
340 
341  logCPAlgTextCfg.info("Default algorithms:")
342  config.printAlgs(printOpts=True)
343 
344  logCPAlgTextCfg.info("Configuring algorithms based on YAML file:")
345  configSeq = config.configure()
346 
347  # defaults are added to config as algs are configured
348  logCPAlgTextCfg.info("Configuration used:")
349  config.printConfig()
350 
351  # compile
352  configAccumulator = ConfigAccumulator(algSeq=algSeq, dataType=dataType, isPhyslite=isPhyslite, geometry=geometry, autoconfigFromFlags=autoconfigFromFlags, flags=flags, noSystematics=noSystematics)
353  configSeq.fullConfigure(configAccumulator)
354 
355  # blocks can be reordered during configSeq.fullConfigure
356  logCPAlgTextCfg.info("ConfigBlocks and their configuration:")
357  configSeq.printOptions()
358 
359  from AnaAlgorithm.DualUseConfig import isAthena, useComponentAccumulator
360  if isAthena and useComponentAccumulator:
361  return configAccumulator.CA
362  else:
363  return None
364 
365 
366 # Combine configuration files
367 #
368 # See the README for more info on how this works
369 #
370 def combineConfigFiles(local, config_path, fragment_key="include"):
371  combined = False
372 
373  # if this isn't an iterable there's nothing to combine
374  if isinstance(local, dict):
375  to_combine = local.values()
376  elif isinstance(local, list):
377  to_combine = local
378  else:
379  return combined
380 
381  # otherwise descend into all the entries here
382  for sub in to_combine:
383  combined = combineConfigFiles(sub, config_path, fragment_key=fragment_key) or combined
384 
385  # if there are no fragments to include we're done
386  if fragment_key not in local:
387  return combined
388 
389  fragment_path = _find_fragment(
390  pathlib.Path(local[fragment_key]),
391  config_path)
392 
393  with open(fragment_path) as fragment_file:
394  # once https://github.com/yaml/pyyaml/issues/173 is resolved
395  # pyyaml will support the yaml 1.2 spec, which is compatable
396  # with json. Until then yaml and json behave differently, so
397  # we have this override.
398  if fragment_path.suffix == '.json':
399  fragment = json.load(fragment_file)
400  else:
401  fragment = yaml.safe_load(fragment_file)
402 
403  # fill out any sub-fragments, looking in the parent path of the
404  # fragment for local sub-fragments.
406  fragment,
407  fragment_path.parent,
408  fragment_key=fragment_key
409  )
410 
411  # merge the fragment with this one
412  _merge_dicts(local, fragment)
413 
414  # delete the fragment so we don't stumble over it again
415  del local[fragment_key]
416 
417 
418  # if we came to here we merged a fragment, so return True
419  return True
420 
421 
422 def _find_fragment(fragment_path, config_path):
423  paths_to_check = [
424  fragment_path,
425  config_path / fragment_path,
426  *[x / fragment_path for x in os.environ["DATAPATH"].split(":")]
427  ]
428  for path in paths_to_check:
429  if path.exists():
430  return path
431 
432  raise FileNotFoundError(fragment_path)
433 
434 
435 def _merge_dicts(local, fragment):
436  # in the list case append the fragment to the local list
437  if isinstance(local, list):
438  local += fragment
439  return
440  # In the dict case, append only missing values to local: the local
441  # values take precidence over the fragment ones.
442  if isinstance(local, dict):
443  for key, value in fragment.items():
444  if key in local:
445  _merge_dicts(local[key], value)
446  else:
447  local[key] = value
448  return
python.ConfigText.TextConfig
Definition: ConfigText.py:35
python.ConfigText.combineConfigFiles
def combineConfigFiles(local, config_path, fragment_key="include")
Definition: ConfigText.py:370
python.ConfigText.TextConfig.saveYaml
def saveYaml(self, filePath='config.yaml', default_flow_style=False, **kwargs)
Definition: ConfigText.py:162
dumpHVPathFromNtuple.append
bool append
Definition: dumpHVPathFromNtuple.py:91
python.ConfigText.TextConfig.cleanupPlaceholders
def cleanupPlaceholders(self, config)
Definition: ConfigText.py:90
python.ConfigText.readYaml
def readYaml(yamlPath)
Definition: ConfigText.py:21
python.ConfigText.TextConfig._configureAlg
def _configureAlg(self, block, blockConfig, configSeq=None, containerName=None, extraOptions=None)
Definition: ConfigText.py:257
python.ConfigText.TextConfig.configure
def configure(self)
Definition: ConfigText.py:210
python.ConfigText.TextConfig._config
_config
Definition: ConfigText.py:49
python.ConfigText.TextConfig.setOptions
def setOptions(self, **kwargs)
Definition: ConfigText.py:199
python.ConfigText._find_fragment
def _find_fragment(fragment_path, config_path)
Definition: ConfigText.py:422
python.ConfigText.printYaml
def printYaml(d, sort=False, jsonFormat=False)
Definition: ConfigText.py:30
python.ConfigText.TextConfig.__init__
def __init__(self, yamlPath=None, *config=None, addDefaultBlocks=True)
Definition: ConfigText.py:36
python.ConfigText.TextConfig.__loadedYaml
__loadedYaml
Definition: ConfigText.py:51
histSizes.list
def list(name, path='/')
Definition: histSizes.py:38
CxxUtils::set
constexpr std::enable_if_t< is_bitmask_v< E >, E & > set(E &lhs, E rhs)
Convenience function to set bits in a class enum bitmask.
Definition: bitmask.h:232
print
void print(char *figname, TCanvas *c1)
Definition: TRTCalib_StrawStatusPlots.cxx:25
TCS::join
std::string join(const std::vector< std::string > &v, const char c=',')
Definition: Trigger/TrigT1/L1Topo/L1TopoCommon/Root/StringUtils.cxx:10
python.ConfigText.TextConfig.preprocessConfig
def preprocessConfig(self, config, algs)
Definition: ConfigText.py:66
Trk::open
@ open
Definition: BinningType.h:40
python.ConfigText.makeSequence
def makeSequence(configPath, *flags=None, algSeq=None, noSystematics=None, dataType=None, geometry=None, autoconfigFromFlags=None, isPhyslite=None, noPhysliteBroken=False)
Definition: ConfigText.py:318
get
T * get(TKey *tobj)
get a TObject* from a TKey* (why can't a TObject be a TKey?)
Definition: hcg.cxx:127
python.utility.LHE.merge
def merge(input_file_pattern, output_file)
Merge many input LHE files into a single output file.
Definition: LHE.py:29
python.ConfigText._merge_dicts
def _merge_dicts(local, fragment)
Definition: ConfigText.py:435
python.ConfigText.TextConfig.loadConfig
def loadConfig(self, yamlPath=None, *configDict=None)
Definition: ConfigText.py:101
python.ConfigText.TextConfig.setConfig
def setConfig(self, config)
Definition: ConfigText.py:58
python.Bindings.keys
keys
Definition: Control/AthenaPython/python/Bindings.py:801
python.ConfigText.TextConfig.printConfig
def printConfig(self, sort=False, jsonFormat=False)
Definition: ConfigText.py:154
python.ConfigText.TextConfig.addBlock
def addBlock(self, name, **kwargs)
Definition: ConfigText.py:174
python.ConfigText.TextConfig._addNewConfigBlocks
def _addNewConfigBlocks(self, modulePath, functionName, algName, defaults=None, pos=None, superBlocks=None)
Definition: ConfigText.py:237
Trk::split
@ split
Definition: LayerMaterialProperties.h:38
merge
Definition: merge.py:1
python.ConfigText.TextConfig._last
_last
Definition: ConfigText.py:55