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 
61  def loadConfig(self, yamlPath):
62  """
63  read a YAML file. Will combine with any config blocks added using python
64  """
65  if self.__loadedYaml or isinstance(yamlPath, list):
66  raise NotImplementedError("Mering multiple yaml files is not implemented.")
67  self.__loadedYaml = True
68 
69  def merge(config, algs, path=''):
70  """Add to config block-by-block"""
71  if not isinstance(config, list):
72  config = [config]
73  # loop over list of blocks with same block name
74  for blocks in config:
75  # deal with case where empty dict is config
76  if blocks == {} and path:
77  self.addBlock(path)
78  return
79  # remove any subBlocks from block config
80  subBlocks = {}
81  for blockName in algs:
82  if blockName in blocks:
83  subBlocks[blockName] = blocks.pop(blockName)
84  # anything left should be a block and it's configuration
85  if blocks:
86  self.addBlock(path, **blocks)
87  # add in any subBlocks
88  for subName, subBlock in subBlocks.items():
89  newPath = f'{path}.{subName}' if path else subName
90  merge(subBlock, algs[subName].subAlgs, newPath)
91  return
92 
93  logCPAlgTextCfg.info(f'loading {yamlPath}')
94  config = readYaml(yamlPath)
95  # check if blocks are defined in yaml file
96  if "AddConfigBlocks" in config:
97  self._configureAlg(self._algs["AddConfigBlocks"], config["AddConfigBlocks"])
98  merge(config, self._algs)
99  return
100 
101 
102  def printConfig(self, sort=False, jsonFormat=False):
103  """Print YAML configuration file."""
104  if self._config is None:
105  raise ValueError("No configuration has been loaded.")
106  printYaml(self._config, sort, jsonFormat)
107  return
108 
109 
110  def saveYaml(self, filePath='config.yaml', default_flow_style=False,
111  **kwargs):
112  """
113  Convert dictionary representation to yaml and save
114  """
115  logCPAlgTextCfg.info(f"Saving configuration to {filePath}")
116  config = self._config
117  with open(filePath, 'w') as outfile:
118  yaml.dump(config, outfile, default_flow_style=False, **kwargs)
119  return
120 
121 
122  def addBlock(self, name, **kwargs):
123  """
124  Create entry into dictionary representing the text configuration
125  """
126  def setEntry(name, config, opts):
127  if '.' not in name:
128  if name not in config:
129  config[name] = opts
130  elif isinstance(config[name], list):
131  config[name].append(opts)
132  else:
133  config[name] = [config[name], opts]
134  # set last added block for setOptionValue
135  self._last = opts
136  else:
137  name, rest = name[:name.index('.')], name[name.index('.') + 1:]
138  config = config[name]
139  if isinstance(config, list):
140  config = config[-1]
141  setEntry(rest, config, opts)
142  return
143  setEntry(name, self._config, dict(kwargs))
144  return
145 
146 
147  def setOptions(self, **kwargs):
148  """
149  Set option(s) for the lsat block that was added. If an option
150  was added previously, will update value
151  """
152  if self._last is None:
153  raise TypeError("Cannot set options before adding a block")
154  # points to dict with opts for last added block
155  self._last.update(**kwargs)
156 
157 
158  def configure(self):
159  """Process YAML configuration file and confgure added algorithms."""
160  # make sure all blocks in yaml file are added (otherwise they would be ignored)
161  for blockName in self._config:
162  if blockName not in self._order[self.ROOTNAME]:
163  raise ValueError(f"Unkown block {blockName} in yaml file")
164 
165  # configure blocks
166  configSeq = ConfigSequence()
167  for blockName in self._order[self.ROOTNAME]:
168  if blockName == "AddConfigBlocks":
169  continue
170 
171  assert blockName in self._algs
172 
173  # order only applies to root blocks
174  if blockName in self._config:
175  blockConfig = self._config[blockName]
176  alg = self._algs[blockName]
177  self._configureAlg(alg, blockConfig, configSeq)
178  else:
179  continue
180  return configSeq
181 
182 
183  def _addNewConfigBlocks(self, modulePath, functionName,
184  algName, defaults=None, pos=None, superBlocks=None):
185  """
186  Load <functionName> from <modulePath>
187  """
188  try:
189  module = importlib.import_module(modulePath)
190  fxn = getattr(module, functionName)
191  except ModuleNotFoundError as e:
192  raise ModuleNotFoundError(f"{e}\nFailed to load {functionName} from {modulePath}")
193  else:
194  sys.modules[functionName] = fxn
195  # add new algorithm to available algorithms
196  self.addAlgConfigBlock(algName=algName, alg=fxn,
197  defaults=defaults,
198  superBlocks=superBlocks,
199  pos=pos)
200  return
201 
202 
203  def _configureAlg(self, block, blockConfig, configSeq=None, containerName=None):
204  if not isinstance(blockConfig, list):
205  blockConfig = [blockConfig]
206 
207  for options in blockConfig:
208  # Special case: propogate containerName down to subAlgs
209  if 'containerName' in options:
210  containerName = options['containerName']
211  elif containerName is not None and 'containerName' not in options:
212  options['containerName'] = containerName
213  # will check which options are associated alg and not options
214  logCPAlgTextCfg.info(f"Configuring {block.algName}")
215  seq, funcOpts = block.makeConfig(options)
216  if not seq._blocks:
217  continue
218  algOpts = seq.setOptions(options)
219  if configSeq is not None:
220  configSeq += seq
221 
222  # check to see if there are unused parameters
223  algOpts = [i['name'] for i in algOpts]
224  expectedOptions = set(funcOpts)
225  expectedOptions |= set(algOpts)
226  expectedOptions |= set(block.subAlgs)
227 
228  difference = set(options.keys()) - expectedOptions
229  if difference:
230  difference = "\n".join(difference)
231  raise ValueError(f"There are options set that are not used for "
232  f"{block.algName}:\n{difference}\n"
233  "Please check your configuration.")
234 
235  # check for sub-blocks and call this function recursively
236  for alg in self._order.get(block.algName, []):
237  if alg in options:
238  subAlg = block.subAlgs[alg]
239  self._configureAlg(subAlg, options[alg], configSeq, containerName)
240  return configSeq
241 
242 
243 def makeSequence(configPath, dataType, algSeq, geometry=None, autoconfigFromFlags=None,
244  isPhyslite=False, noPhysliteBroken=False, noSystematics=None):
245  """
246  """
247 
248  from AnalysisAlgorithmsConfig.ConfigAccumulator import ConfigAccumulator
249 
250  config = TextConfig(configPath)
251 
252  logCPAlgTextCfg.info("Configuration file read in:")
253  config.printConfig()
254 
255  logCPAlgTextCfg.info("Default algorithms:")
256  config.printAlgs(printOpts=True)
257 
258  logCPAlgTextCfg.info("Configuring algorithms based on YAML file:")
259  configSeq = config.configure()
260 
261  # defaults are added to config as algs are configured
262  logCPAlgTextCfg.info("Configuration used:")
263  config.printConfig()
264 
265  # compile
266  configAccumulator = ConfigAccumulator(algSeq, dataType, isPhyslite=isPhyslite, geometry=geometry, autoconfigFromFlags=autoconfigFromFlags, noSystematics=noSystematics)
267  configSeq.fullConfigure(configAccumulator)
268 
269  # blocks can be reordered during configSeq.fullConfigure
270  logCPAlgTextCfg.info("ConfigBlocks and their configuration:")
271  configSeq.printOptions()
272 
273  from AnaAlgorithm.DualUseConfig import isAthena, useComponentAccumulator
274  if isAthena and useComponentAccumulator:
275  return configAccumulator.CA
276  else:
277  return None
278 
279 
280 # Combine configuration files
281 #
282 # See the README for more info on how this works
283 #
284 def combineConfigFiles(local, config_path, fragment_key="include"):
285 
286  # if this isn't an iterable there's nothing to combine
287  if isinstance(local, dict):
288  to_combine = local.values()
289  elif isinstance(local, list):
290  to_combine = local
291  else:
292  return
293 
294  # otherwise descend into all the entries here
295  for sub in to_combine:
296  combineConfigFiles(sub, config_path, fragment_key=fragment_key)
297 
298  # if there are no fragments to include we're done
299  if fragment_key not in local:
300  return
301 
302  fragment_path = _find_fragment(
303  pathlib.Path(local[fragment_key]),
304  config_path)
305 
306  with open(fragment_path) as fragment_file:
307  # once https://github.com/yaml/pyyaml/issues/173 is resolved
308  # pyyaml will support the yaml 1.2 spec, which is compatable
309  # with json. Until then yaml and json behave differently, so
310  # we have this override.
311  if fragment_path.suffix == '.json':
312  fragment = json.load(fragment_file)
313  else:
314  fragment = yaml.safe_load(fragment_file)
315 
316  # fill out any sub-fragments, looking in the parent path of the
317  # fragment for local sub-fragments.
319  fragment,
320  fragment_path.parent,
321  fragment_key=fragment_key
322  )
323 
324  # merge the fragment with this one
325  _merge_dicts(local, fragment)
326 
327  # delete the fragment so we don't stumble over it again
328  del local[fragment_key]
329 
330 
331 def _find_fragment(fragment_path, config_path):
332  paths_to_check = [
333  fragment_path,
334  config_path / fragment_path,
335  *[x / fragment_path for x in os.environ["DATAPATH"].split(":")]
336  ]
337  for path in paths_to_check:
338  if path.exists():
339  return path
340 
341  raise FileNotFoundError(fragment_path)
342 
343 
344 def _merge_dicts(local, fragment):
345  # in the list case append the fragment to the local list
346  if isinstance(local, list):
347  local += fragment
348  return
349  # In the dict case, append only missing values to local: the local
350  # values take precidence over the fragment ones.
351  if isinstance(local, dict):
352  for key, value in fragment.items():
353  if key in local:
354  _merge_dicts(local[key], value)
355  else:
356  local[key] = value
357  return
python.ConfigText.TextConfig
Definition: ConfigText.py:33
python.ConfigText.combineConfigFiles
def combineConfigFiles(local, config_path, fragment_key="include")
Definition: ConfigText.py:284
python.ConfigText.TextConfig.saveYaml
def saveYaml(self, filePath='config.yaml', default_flow_style=False, **kwargs)
Definition: ConfigText.py:110
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:243
python.ConfigText.readYaml
def readYaml(yamlPath)
Definition: ConfigText.py:19
python.ConfigText.TextConfig.configure
def configure(self)
Definition: ConfigText.py:158
python.ConfigText.TextConfig._config
_config
Definition: ConfigText.py:44
python.ConfigText.TextConfig.setOptions
def setOptions(self, **kwargs)
Definition: ConfigText.py:147
python.ConfigText._find_fragment
def _find_fragment(fragment_path, config_path)
Definition: ConfigText.py:331
python.ConfigText.printYaml
def printYaml(d, sort=False, jsonFormat=False)
Definition: ConfigText.py:28
python.ConfigText.TextConfig.__loadedYaml
__loadedYaml
Definition: ConfigText.py:46
python.ConfigText.TextConfig._configureAlg
def _configureAlg(self, block, blockConfig, configSeq=None, containerName=None)
Definition: ConfigText.py:203
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:224
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.loadConfig
def loadConfig(self, yamlPath)
Definition: ConfigText.py:61
Trk::open
@ open
Definition: BinningType.h:40
dqt_zlumi_pandas.update
update
Definition: dqt_zlumi_pandas.py:42
Muon::print
std::string print(const MuPatSegment &)
Definition: MuonTrackSteering.cxx:28
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:17
python.ConfigText._merge_dicts
def _merge_dicts(local, fragment)
Definition: ConfigText.py:344
python.ConfigText.TextConfig.setConfig
def setConfig(self, config)
Definition: ConfigText.py:53
python.ConfigText.TextConfig.printConfig
def printConfig(self, sort=False, jsonFormat=False)
Definition: ConfigText.py:102
python.ConfigText.TextConfig.addBlock
def addBlock(self, name, **kwargs)
Definition: ConfigText.py:122
python.ConfigText.TextConfig._addNewConfigBlocks
def _addNewConfigBlocks(self, modulePath, functionName, algName, defaults=None, pos=None, superBlocks=None)
Definition: ConfigText.py:183
Trk::split
@ split
Definition: LayerMaterialProperties.h:38
merge
Definition: merge.py:1
python.ConfigText.TextConfig._last
_last
Definition: ConfigText.py:50