ATLAS Offline Software
Loading...
Searching...
No Matches
CPBaseRunner.py
Go to the documentation of this file.
1# Copyright (C) 2002-2025 CERN for the benefit of the ATLAS collaboration
2
3import argparse
4from AnaAlgorithm.Logging import logging
5from abc import ABC, abstractmethod
6import os
7
8
9class CPBaseRunner(ABC):
10 def __init__(self):
11 self.logger = logging.getLogger("CPBaseRunner")
12 self._args = None
13 self._inputList = None
15 self.rawConfig = None
16 # parse the arguments here is a bad idea
17
18 @property
19 def args(self):
20 if self._args is None:
21 self._args = self.parser.parse_args()
22 return self._args
23
24 @property
25 def inputList(self):
26 if self._inputList is None:
27 if self.args.input_list.endswith('.txt'):
28 self._inputList = CPBaseRunner._parseInputFileList(
29 self.args.input_list)
30 elif ".root" in self.args.input_list:
31 self._inputList = [self.args.input_list]
32 else:
33 raise FileNotFoundError(f'Input file list \"{self.args.input_list}\" is not supported!'
34 'Please provide a text file with a list of input files or a single root file.')
35 self.logger.info("Initialized input files: %s", self._inputList)
36 return self._inputList
37
38 @property
39 def outputName(self):
40 if self.args.output_name.endswith('.root'):
41 return self.args.output_name[:-5]
42 else:
43 return self.args.output_name
44
45 def printFlags(self):
46 self.logger.info("="*73)
47 self.logger.info("="*20 + "FLAG CONFIGURATION" + "="*20)
48 self.logger.info("="*73)
49 self.logger.info(" Input files: %s", self.flags.Input.isMC)
50 self.logger.info(" RunNumber: %s",
51 self.flags.Input.RunNumbers)
52 self.logger.info(" MCCampaign: %s",
53 self.flags.Input.MCCampaign)
54 self.logger.info(" GeneratorInfo: %s",
55 self.flags.Input.GeneratorsInfo)
56 self.logger.info(" MaxEvents: %s", self.flags.Exec.MaxEvents)
57 self.logger.info(" SkipEvents: %s", self.flags.Exec.SkipEvents)
58 self.logger.info("="*73)
59
60 @abstractmethod
62 pass
63
64 @abstractmethod
65 def makeAlgSequence(self):
66 pass
67
68 @abstractmethod
69 def run(self):
70 pass
71
72 # The responsiblity of flag.lock will pass to the caller
74 from AthenaConfiguration.AllConfigFlags import initConfigFlags
75 flags = initConfigFlags()
76 flags.Input.Files = self.inputList
77 flags.Exec.MaxEvents = self.args.max_events
78 flags.Exec.SkipEvents = self.args.skip_n_events
79 return flags
80
82 parser = argparse.ArgumentParser(
83 description='Runscript for CP Algorithm unit tests')
84 baseGroup = parser.add_argument_group('Base Script Options')
85 baseGroup.add_argument('-i', '--input-list', dest='input_list',
86 help='path to text file containing list of input files, or a single root file')
87 baseGroup.add_argument('-o', '--output-name', dest='output_name', default='output',
88 help='output name of the analysis root file')
89 baseGroup.add_argument('-e', '--max-events', dest='max_events', type=int, default=-1,
90 help='Number of events to run')
91 baseGroup.add_argument('-t', '--text-config', dest='text_config',
92 help='path to the YAML configuration file. Tips: use atlas_install_data(path/to/*.yaml) in CMakeLists.txt can help locating the config just by the config file name.')
93 baseGroup.add_argument('--no-systematics', dest='no_systematics',
94 action='store_true', help='Disable systematics')
95 baseGroup.add_argument('--skip-n-events', dest='skip_n_events', type=int, default=0,
96 help='Skip the first N events in the run, not first N events for each file. This is meant for debugging only. \nIn Eventloop, this option disable the cutbookkeeper algorithms due to technical reasons, and can only be ran in direct-driver.')
97 return parser
98
99 def _mergeYamlconfig(self, yaml_path):
100 with open(yaml_path, "r", encoding="utf-8") as cfg_file:
101 import yaml
102 config_data = yaml.safe_load(cfg_file)
103 from AnalysisAlgorithmsConfig.ConfigText import combineConfigFiles
104 combined = combineConfigFiles(
105 config_data,
106 os.path.dirname(os.path.dirname(yaml_path)),
107 fragment_key="include",
108 )
109 if combined:
110 with open("merged_config.yaml", "w") as cfg:
111 cfg.write(yaml.dump(config_data))
112 self.logger.info("Merged included fragments into main config.")
113 return config_data, combined
114
116 yamlconfig = self._findYamlConfig(local=True)
117 if yamlconfig is None:
118 raise FileNotFoundError(f'Failed to locate \"{self.args.text_config}\" config file!'
119 'Check if you have a typo in -t/--text-config argument or missing file in the analysis configuration sub-directory.')
120 self.logger.info(f"Found YAML config at: {yamlconfig}")
121 self.logger.info("Setting up configuration based on YAML config:")
122
123 from AnalysisAlgorithmsConfig.ConfigText import TextConfig
124 self.rawConfig, merged = self._mergeYamlconfig(yamlconfig)
125 self.modifyYamlConfig()
126 config = TextConfig(config=self.rawConfig)
127 return config
128
129 def _findYamlConfig(self, local=True):
130 # Find local and abs path first
131 if local and ((yamlConfig := CPBaseRunner.findLocalPathYamlConfig(self.args.text_config)) is not None):
132 return yamlConfig
133 # Then search in the analysis repository and warn for duplicates
134 elif (yamlConfig := CPBaseRunner.findRepoPathYamlConfig(self.args.text_config)):
135 if len(yamlConfig) > 1:
136 raise FileExistsError(
137 f'Multiple files named \"{self.args.text_config}\" found in the analysis repository. Please provide a more specific path to the config file.\nMatches found:\n' + '\n'.join(yamlConfig))
138 else:
139 return yamlConfig[0]
140 # Finally try the slowest method using AthenaCommon
141 else:
142 from AthenaCommon.Utils.unixtools import find_datafile
143 return find_datafile(self.args.text_config)
144
145 @staticmethod
146 def findLocalPathYamlConfig(textConfigPath):
147 configPath = os.path.normpath(os.path.expanduser(textConfigPath))
148 if os.path.isabs(configPath) and os.path.isfile(configPath):
149 return configPath
150 cwdPath = os.path.join(os.getcwd(), configPath)
151 if os.path.isfile(cwdPath):
152 return cwdPath
153 return None
154
155 @staticmethod
156 def findRepoPathYamlConfig(textConfigPath):
157 """
158 Search for the file up to two levels deep within the first DATAPATH entry.
159 First, check directly under the analysis repository (depth 0).
160 Then, check immediate subdirectories (depth 1), looking for the file inside each.
161 Returns a list of all matches found.
162 """
163 matches = []
164 analysisRepoPath = os.environ.get('DATAPATH', '').split(os.pathsep)[0]
165 # Depth 0: Directly under analysisRepoPath
166 searchPath = os.path.join(analysisRepoPath, textConfigPath)
167 if os.path.isfile(searchPath):
168 matches.append(searchPath)
169 # Depth 1: Inside immediate subdirectories
170 try:
171 for subdir in os.listdir(analysisRepoPath):
172 candidate = os.path.join(
173 analysisRepoPath, subdir, textConfigPath)
174 if os.path.isfile(candidate):
175 matches.append(candidate)
176 except Exception:
177 pass
178 return matches
179
181 files = []
182 with open(path, 'r') as inputText:
183 for line in inputText.readlines():
184 # Strip the line and skip comments and empty lines
185 line = line.strip()
186 if line.startswith('#') or not line:
187 continue
188 if os.path.isdir(line):
189 if not os.listdir(line):
190 raise FileNotFoundError(
191 f"The directory \"{path}\" is empty. Please provide a directory with .root files.")
192 for root_file in os.listdir(line):
193 if '.root' in root_file:
194 files.append(os.path.join(line, root_file))
195 else:
196 files += line.split(',')
197 # Remove leading/trailing whitespaces from file names
198 files = [file.strip() for file in files]
199 return files
200
201 def setup(self):
204 self.parser.parse_args()
206
208 self.parser.description = 'CPRunScript available arguments'
209 self.parser.usage = argparse.SUPPRESS
210 self.parser.print_help()
211
212 # Three customization hooks will be ran in the order below
213 # First: modify parser arguments, have access to self.parser, and self.flags (Athena flags or EL flags)
214 # Second: modify Yaml config, have access to self.rawConfig, and self.flags, self.parser, self.config
215 # Third: modify algorithm sequence, have access to self.flags, self.config, self.args, and self.parser, , (self.algseq / self.configSeq)
216 def modifyParserArguments(self): # noqa: B027
217 # Example: self.parser.add_argument('--no-filter', dest='no_filter', action='store_true', help='Disable filtering')
218 # The seemingly trivial log is to prevent CI from complaining about empty hook functions
219 pass
220
221 def modifyYamlConfig(self): # noqa: B027
222 # Example: self.rawConfig['SomeSection']['SomeOption'] = some_value
223 # The seemingly trivial log is to prevent CI from complaining about empty hook functions
224 pass
225
226 def modifyAlgSequence(self): # noqa: B027
227 # For AthAnalysis: self.configSeq.some_attribute = some_value
228 # For EventLoop: self.algSeq.some_attribute = some_value
229 # The seemingly trivial log is to prevent CI from complaining about empty hook functions
230 pass
findLocalPathYamlConfig(textConfigPath)
std::vector< std::string > split(const std::string &s, const std::string &t=":")
Definition hcg.cxx:177