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