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
8class CPBaseRunner(ABC):
9 def __init__(self):
10 self.logger = logging.getLogger("CPBaseRunner")
11 self._args = None
12 self._inputList = None
14 # parse the arguments here is a bad idea
15
16 @property
17 def args(self):
18 if self._args is None:
19 self._args = self.parser.parse_args()
20 return self._args
21
22 @property
23 def inputList(self):
24 if self._inputList is None:
25 if self.args.input_list.endswith('.txt'):
26 self._inputList = CPBaseRunner._parseInputFileList(self.args.input_list)
27 elif ".root" in self.args.input_list:
28 self._inputList = [self.args.input_list]
29 else:
30 raise FileNotFoundError(f'Input file list \"{self.args.input_list}\" is not supported!'
31 'Please provide a text file with a list of input files or a single root file.')
32 self.logger.info("Initialized input files: %s", self._inputList)
33 return self._inputList
34
35 @property
36 def outputName(self):
37 if self.args.output_name.endswith('.root'):
38 return self.args.output_name[:-5]
39 else:
40 return self.args.output_name
41
42 def printFlags(self):
43 self.logger.info("="*73)
44 self.logger.info("="*20 + "FLAG CONFIGURATION" + "="*20)
45 self.logger.info("="*73)
46 self.logger.info(" Input files: %s", self.flags.Input.isMC)
47 self.logger.info(" RunNumber: %s", self.flags.Input.RunNumbers)
48 self.logger.info(" MCCampaign: %s", self.flags.Input.MCCampaign)
49 self.logger.info(" GeneratorInfo: %s", self.flags.Input.GeneratorsInfo)
50 self.logger.info(" MaxEvents: %s", self.flags.Exec.MaxEvents)
51 self.logger.info(" SkipEvents: %s", self.flags.Exec.SkipEvents)
52 self.logger.info("="*73)
53
54 @abstractmethod
56 pass
57
58 @abstractmethod
59 def makeAlgSequence(self):
60 pass
61
62 @abstractmethod
63 def run(self):
64 pass
65
66 # The responsiblity of flag.lock will pass to the caller
68 from AthenaConfiguration.AllConfigFlags import initConfigFlags
69 flags = initConfigFlags()
70 flags.Input.Files = self.inputList
71 flags.Exec.MaxEvents = self.args.max_events
72 flags.Exec.SkipEvents = self.args.skip_n_events
73 return flags
74
76 parser = argparse.ArgumentParser(
77 description='Runscript for CP Algorithm unit tests')
78 baseGroup = parser.add_argument_group('Base Script Options')
79 baseGroup.add_argument('-i', '--input-list', dest='input_list',
80 help='path to text file containing list of input files, or a single root file')
81 baseGroup.add_argument('-o','--output-name', dest='output_name', default='output',
82 help='output name of the analysis root file')
83 baseGroup.add_argument('-e', '--max-events', dest='max_events', type=int, default=-1,
84 help='Number of events to run')
85 baseGroup.add_argument('-t', '--text-config', dest='text_config',
86 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.')
87 baseGroup.add_argument('--no-systematics', dest='no_systematics',
88 action='store_true', help='Disable systematics')
89 baseGroup.add_argument('--skip-n-events', dest='skip_n_events', type=int, default=0,
90 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.')
91 return parser
92
93 def _readYamlConfig(self):
94 yamlconfig = self._findYamlConfig(local=True)
95 if yamlconfig is None:
96 raise FileNotFoundError(f'Failed to locate \"{self.args.text_config}\" config file!'
97 'Check if you have a typo in -t/--text-config argument or missing file in the analysis configuration sub-directory.')
98 self.logger.info(f"Found YAML config at: {yamlconfig}")
99 self.logger.info("Setting up configuration based on YAML config:")
100 from AnalysisAlgorithmsConfig.ConfigText import TextConfig
101 config = TextConfig(yamlconfig)
102 return config
103
104
105 def _findYamlConfig(self, local=True):
106 # Find local and abs path first
107 if local and ((yamlConfig := CPBaseRunner.findLocalPathYamlConfig(self.args.text_config)) is not None):
108 return yamlConfig
109 # Then search in the analysis repository and warn for duplicates
110 elif (yamlConfig := CPBaseRunner.findRepoPathYamlConfig(self.args.text_config)):
111 if len(yamlConfig) > 1:
112 raise FileExistsError(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))
113 else:
114 return yamlConfig[0]
115 # Finally try the slowest method using AthenaCommon
116 else:
117 from AthenaCommon.Utils.unixtools import find_datafile
118 return find_datafile(self.args.text_config)
119
120 @staticmethod
121 def findLocalPathYamlConfig(textConfigPath):
122 configPath = os.path.normpath(os.path.expanduser(textConfigPath))
123 if os.path.isabs(configPath) and os.path.isfile(configPath):
124 return configPath
125 cwdPath = os.path.join(os.getcwd(), configPath)
126 if os.path.isfile(cwdPath):
127 return cwdPath
128 return None
129
130 @staticmethod
131 def findRepoPathYamlConfig(textConfigPath):
132 """
133 Search for the file up to two levels deep within the first DATAPATH entry.
134 First, check directly under the analysis repository (depth 0).
135 Then, check immediate subdirectories (depth 1), looking for the file inside each.
136 Returns a list of all matches found.
137 """
138 matches = []
139 analysisRepoPath = os.environ.get('DATAPATH', '').split(os.pathsep)[0]
140 # Depth 0: Directly under analysisRepoPath
141 searchPath = os.path.join(analysisRepoPath, textConfigPath)
142 if os.path.isfile(searchPath):
143 matches.append(searchPath)
144 # Depth 1: Inside immediate subdirectories
145 try:
146 for subdir in os.listdir(analysisRepoPath):
147 candidate = os.path.join(analysisRepoPath, subdir, textConfigPath)
148 if os.path.isfile(candidate):
149 matches.append(candidate)
150 except Exception:
151 pass
152 return matches
153
155 files = []
156 with open(path, 'r') as inputText:
157 for line in inputText.readlines():
158 # Strip the line and skip comments and empty lines
159 line = line.strip()
160 if line.startswith('#') or not line:
161 continue
162 if os.path.isdir(line):
163 if not os.listdir(line):
164 raise FileNotFoundError(f"The directory \"{path}\" is empty. Please provide a directory with .root files.")
165 for root_file in os.listdir(line):
166 if '.root' in root_file:
167 files.append(os.path.join(line, root_file))
168 else:
169 files += line.split(',')
170 # Remove leading/trailing whitespaces from file names
171 files = [file.strip() for file in files]
172 return files
173
174 def setup(self):
175 self.parser.parse_args()
178
180 self.parser.description = 'CPRunScript available arguments'
181 self.parser.usage = argparse.SUPPRESS
182 self.parser.print_help()
findLocalPathYamlConfig(textConfigPath)
std::vector< std::string > split(const std::string &s, const std::string &t=":")
Definition hcg.cxx:177
Definition run.py:1