ATLAS Offline Software
Loading...
Searching...
No Matches
CPGridRun.py
Go to the documentation of this file.
1#! /usr/bin/env python
2
3# Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
4import argparse
5import os
6import sys
7from pathlib import Path
8
9from AnaAlgorithm.DualUseConfig import isAthena
10from AnaAlgorithm.Logging import logging
11
12logCPGridRun = logging.getLogger('CPGridRun')
14 def __init__(self):
15 self._errorCollector = {} # Delay the error collection until the end of the script for better user experience
16 self._runscript = None
17 self._tarfile = 'cpgrid.tar.gz'
18 self._isFirstRun = True
19 self._tarballRecreated = False
20 self._inputList = None
21 self._inputNames = None
22 self._yamlPath = None
23 self.cmd = {} # sample name -> command
24 self.outputs = {} # sample name -> output dataset name
25
28
29 if self.args.help:
30 self._initRunscript()
31 self.printHelp()
32 sys.exit(0)
33
34 def _initRunscript(self):
35 if self._runscript is not None:
36 return self._runscript
37 elif isAthena:
38 from AnalysisAlgorithmsConfig.AthenaCPRunScript import AthenaCPRunScript
39 self._runscript = AthenaCPRunScript()
40 else:
41 from AnalysisAlgorithmsConfig.EventLoopCPRunScript import EventLoopCPRunScript
42 self._runscript = EventLoopCPRunScript()
43 return self._runscript
44
46 parser = argparse.ArgumentParser(description='CPGrid runscript to submit CPRun.py jobs to the grid. '
47 'This script will submit a job to the grid using files in the input text one by one.'
48 'CPRun.py can handle multiple sources of input and create one output; but not this script',
49 add_help=False,
50 formatter_class=argparse.RawTextHelpFormatter)
51 parser.add_argument('-h', '--help', dest='help', action='store_true', help='Show this help message and continue')
52
53 ioGroup = parser.add_argument_group('Input/Output file configuration')
54 ioGroup.add_argument('-i','--input-list', dest='input_list', help='Path to the text file containing list of containers on the panda grid. Each container will be passed to prun as --inDS and is run individually')
55 ioGroup.add_argument('--output-files', dest='output_files', nargs='+', default=['output.root'],
56 help='The output files of the grid job. Example: --output-files A.root B.txt B.root results in A/A.root, B/B.txt, B/B.root in the output directory. No need to specify if using CPRun.py')
57 ioGroup.add_argument('--destSE', dest='destSE', default='', type=str, help='Destination storage element (PanDA)')
58 ioGroup.add_argument('--mergeType', dest='mergeType', default='Default', type=str, help='Output merging type, [None, Default, xAOD]')
59
60 pandaGroup = parser.add_argument_group('Input/Output naming configuration')
61 pandaGroup.add_argument('--gridUsername', dest='gridUsername', default=os.getenv('USER', ''), type=str, help='Grid username, or the groupname. Default is the current user. Only affect file naming')
62 pandaGroup.add_argument('--prefix', dest='prefix', default='', type=str, help='Prefix for the output directory. Dynamically set with input container if not provided')
63 pandaGroup.add_argument('--suffix', dest='suffix', default='',type=str, help='Suffix for the output directory')
64 pandaGroup.add_argument('--outDS', dest='outDS', default='', type=str,
65 help='Name of an output dataset. outDS will contain all output files (PanDA). If not provided, support dynamic naming if input name is in the Atlas production format or typical user production format')
66
67 cpgridGroup = parser.add_argument_group('CPGrid configuration')
68 cpgridGroup.add_argument('--groupProduction', dest='groupProduction', action='store_true', help='Only use for official production')
69
70 cpgridGroup.add_argument('--exec', dest='exec', type=str,
71 help='Executable line for the CPRun.py or custom script to run on the grid encapsulated in a double quote (PanDA)\n'
72 'Run CPRun.py with preset behavior including streamlined file i/o. E.g, "CPRun.py -t config.yaml --no-systematics".\n'
73 'Run custom script: "customRun.py -i inputs -o output --text-config config.yaml --flagA --flagB"\n'
74 )
75
76 submissionGroup = parser.add_argument_group('Submission configuration')
77 submissionGroup.add_argument('--noSubmit', dest='noSubmit', action='store_true', help='Do not submit the job to the grid (PanDA). Useful to inspect the prun command')
78 submissionGroup.add_argument('--testRun', dest='testRun', action='store_true', help='Will submit job to the grid but greatly limit the number of files per job (10) and number of events (300)')
79 submissionGroup.add_argument('--recreateTar', dest='recreateTar', action='store_true', help='Re-compress the source code. Source code are compressed by default in submission, this is useful when the source code is updated')
80 submissionGroup.add_argument('--useCentralPackage', dest='useCentralPackage', action='store_true', help='Use central package instead of custom packages')
81 submissionGroup.add_argument('--bulk-submission', dest='bulk_submission', action='store_true', help='Submit all containers in the input list as one task.')
82
83 miscGroup = parser.add_argument_group('Miscellaneous configuration')
84 miscGroup.add_argument('-y', '--agreeAll', dest='agreeAll', action='store_true', help='Agree to all the submission details without asking for confirmation. Use with caution!')
85 miscGroup.add_argument('--checkInputDS', dest='checkInputDS', action='store_true', help='Check if the input datasets are available on the AMI.')
86 miscGroup.add_argument('--framework', dest='framework', default='CPGridRun', type=str, help='Declaring a name for your submission for PanDA team to collect statistics. Default is CPGridRun')
87
88 self.args, self.unknown_args = parser.parse_known_args()
90 return parser
91
92 def _createPrunArgsDict(self) -> dict:
93 '''
94 converting unknown args to a dictionary
95 '''
96 unknownArgsDict = self._unknownArgsDict()
97 if unknownArgsDict and self.hasPrun():
98 self._checkPrunArgs(unknownArgsDict)
99 logCPGridRun.info(f"Adding prun exclusive arguments: {unknownArgsDict.keys()}")
100 elif unknownArgsDict:
101 logCPGridRun.warning(f"Unknown arguments detected: {unknownArgsDict}. Cannot check the availablility in Prun because Prun is not available / noSubmit is on.")
102 else:
103 pass
104 return unknownArgsDict
105
106 @property
107 def inputList(self):
108 if not self.args.input_list:
109 raise ValueError('No input list provided, use --input-list to specify the input containers')
110 if self._inputList is None:
111 input_list_path = Path(self.args.input_list)
112 if input_list_path.exists():
113 if input_list_path.suffix == '.txt':
114 self._inputList, self._inputNames = CPGridRun._parseInputFileList(input_list_path, self.args.bulk_submission)
115 elif input_list_path.suffix == '.json':
116 raise NotImplementedError('JSON input list parsing is not implemented')
117 else:
118 raise ValueError('Unsupported input list format, only .txt files are supported.')
119 elif CPGridRun.isAtlasProductionFormat(self.args.input_list):
120 self._inputList = [self.args.input_list]
121 self._inputNames = [None]
122 else:
123 raise ValueError('use --input-list to specify input containers')
124 return self._inputList
125
127 output_files = []
128 for output in self.args.output_files:
129 if ',' in output:
130 output_files.extend(output.split(','))
131 else:
132 output_files.append(output)
133 self.output_files = output_files
134
135 def printHelp(self):
136 self.gridParser.print_help()
137 logCPGridRun.info("\033[92m\n If you are using CPRun.py, the following flags are for the CPRun.py in this framework\033[0m")
138 self._runscript.parser.usage = argparse.SUPPRESS
139 self._runscript.parser.print_help()
140
141 def getParser(self):
142 return self.gridParser
143
144 # This function do all the checking, cleaning and preparing the command to be submitted to the grid
145 # separated for client to be able to change the behavior
147 for input, name in zip(self.inputList, self._inputNames):
148 cmd, config = self.configureSubmissionSingleSample(input, name)
149 self.cmd[input] = cmd
150 self.outputs[input] = config["outDS"]
151 self._isFirstRun = False
152
153 def configureSubmissionSingleSample(self, input, name):
154 config = {
155 'inDS': input,
156 'outDS': self.args.outDS if self.args.outDS else self.outputDSFormatter(input, name),
157 'cmtConfig': os.environ["CMTCONFIG"],
158 'writeInputToTxt': 'IN:in.txt',
159 'outputs': self.outputsFormatter(),
160 'exec': self.execFormatter(),
161 'memory': "2000", # MB
162 'addNthFieldOfInDSToLFN': '2,3,6',
163 'framework': self.args.framework,
164 }
165 if self.args.noSubmit:
166 config['noSubmit'] = True
167
168 if self.args.mergeType == 'xAOD':
169 config['mergeScript'] = 'xAODMerge %OUT `echo %IN | sed \'s/,/ /g\'`'
170
171 if self.args.mergeType != 'None':
172 config['mergeOutput'] = True
173
174 # Three types of files sending the grid
175 if self.args.useCentralPackage: # 1. Using central package and have a yaml file only
176 config['extFile'] = self._yamlPath
177 config['noBuild'] = True
178 config['noCompile'] = True
179 config['athenaTag'] = f"AnalysisBase,{os.environ['AnalysisBase_VERSION']}"
180 elif self._filesChangedOrTarballNotCreated(): # 2. Using custom packages and haven't compressed the tarball since the last changes
181 config['outTarBall'] = self._tarfile
182 config['useAthenaPackages'] = True
183 self._tarballRecreated = True
184 elif self._hasCompressedTarball(): # 3. Using custom packages and have compressed the tarball
185 config['inTarBall'] = self._tarfile
186 config['useAthenaPackages'] = True
187
188 if self.args.groupProduction:
189 config['official'] = True
190 config['voms'] = f'atlas:/atlas/{self.args.gridUsername}/Role=production'
191
192 if self.args.destSE:
193 config['destSE'] = self.args.destSE
194
195 if self.args.testRun:
196 config['nEventsPerFile'] = 100
197 config['nFiles'] = 5
198 config.update(self.prunArgsDict)
199 cmd = 'prun \\\n'
200 for k, v in config.items():
201 if isinstance(v, bool) and v:
202 cmd += f'--{k} \\\n'
203 elif v is not None and v != '':
204 cmd += f'--{k} {v} \\\n'
205 return cmd.rstrip(' \\\n'), config
206
207 def _unknownArgsDict(self)->dict:
208 '''
209 Cleans the unknown args by removing leading dashes and ensuring they are in key-value pairs
210 '''
211 unknown_args_dict = {}
212 idx = 0
213 while idx < len(self.unknown_args):
214 if self.unknown_args[idx].startswith('-'):
215 if idx + 1 < len(self.unknown_args) and not self.unknown_args[idx + 1].startswith('-'):
216 unknown_args_dict[self.unknown_args[idx].lstrip('-')] = self.unknown_args[idx + 1]
217 idx += 2
218 else:
219 unknown_args_dict[self.unknown_args[idx].lstrip('-')] = True
220 idx += 1
221 return unknown_args_dict
222
223 def _checkPrunArgs(self,argDict):
224 '''
225 check the arguments against the prun script to ensure they are valid
226 See https://github.com/PanDAWMS/panda-client/blob/master/pandaclient/PrunScript.py
227 '''
228 import pandaclient.PrunScript
229 # We need to temporarily clear the sys.argv to avoid the parser from PrunScript to fail
230 original_argv = sys.argv
231 sys.argv = ['prun'] # Reset sys.argv to only contain the script name
232 prunArgsDict = {}
233 prunArgsDict = pandaclient.PrunScript.main(get_options=True)
234 sys.argv = original_argv # Restore the original sys.argv
235 nonPrunOrCPGridArgs = []
236 for arg in argDict:
237 if arg not in prunArgsDict:
238 nonPrunOrCPGridArgs.append(arg)
239 if nonPrunOrCPGridArgs:
240 logCPGridRun.error(f"Unknown arguments detected: {nonPrunOrCPGridArgs}. They do not belong to CPGridRun or Panda.")
241 raise ValueError(f"Unknown arguments detected: {nonPrunOrCPGridArgs}. They do not belong to CPGridRun or Panda.")
242
244 for key, cmd in self.cmd.items():
245 parsed_name = CPGridRun.atlasProductionNameParser(key)
246 logCPGridRun.info("\n"
247 f"Input: {key}\n" +
248 "\n".join([f" {k.replace('_', ' ').title()}: {v}" for k, v in parsed_name.items()]))
249 logCPGridRun.info(f"Command: \n{cmd}")
250 print("-" * 70)
251 # Add your submission logic here
252
253 def hasPyami(self):
254 try:
255 global pyAMI
256 import pyAMI.client
257 import pyAMI.atlas.api
258 except ModuleNotFoundError:
259 self._errorCollector['no AMI'] = (
260 "Cannot import pyAMI, please run the following commands:\n\n"
261 "```\n"
262 "lsetup pyami\n"
263 "voms-proxy-init -voms atlas\n"
264 "```\n"
265 "and make sure you have a valid certificate.")
266 return False
267 return True
268
269 def checkInputInPyami(self) -> bool:
270 if not self.hasPyami():
271 return False
272
273 client = pyAMI.client.Client('atlas')
274 pyAMI.atlas.api.init()
275
276 queries, datasetPtag = self._prepareAmiQueryFromInputList()
277 try:
278 results = pyAMI.atlas.api.list_datasets(client, patterns=queries)
279 except pyAMI.exception.Error:
280 self._errorCollector['no valid certificate'] = (
281 "Cannot query AMI, please run 'voms-proxy-init -voms atlas' and ensure your certificate is valid.")
282 return False
283
284 return self._analyzeAmiResults(results, datasetPtag)
285
287 '''
288 Helper function to prepare a list of queries for the AMI based on the input list.
289 It will replace the _p### with _p% to match the latest ptag.
290 '''
291 import re
292 regex = re.compile("_p[0-9]+")
293 queries = []
294 datasetPtag = {}
295 for datasetName in self.cmd:
296 parsed = CPGridRun.atlasProductionNameParser(datasetName)
297 datasetPtag[datasetName] = parsed.get('ptag')
298 queries.append(regex.sub("_p%", datasetName))
299 return queries, datasetPtag
300
301 def _analyzeAmiResults(self, results, datasetPtag) -> bool:
302 import re
303 regex = re.compile("_p[0-9]+")
304 results = [r['ldn'] for r in results]
305 notFound = []
306 latestPtag = {}
307
308 for datasetName in self.cmd:
309 if datasetName not in results:
310 notFound.append(datasetName)
311
312 base = regex.sub("_p%", datasetName)
313 matching = [r for r in results if r.startswith(base.replace("_p%", ""))]
314 for m in matching:
315 mParsed = CPGridRun.atlasProductionNameParser(m)
316 try:
317 mPtagInt = int(mParsed.get('ptag', 'p0')[1:])
318 currentPtagInt = int(datasetPtag.get(datasetName, 'p0')[1:])
319 if mPtagInt > currentPtagInt:
320 latestPtag[datasetName] = f"p{mPtagInt}"
321 except (ValueError, TypeError):
322 continue
323
324 if latestPtag:
325 logCPGridRun.info("Newer version of datasets found in AMI:")
326 for name, ptag in latestPtag.items():
327 logCPGridRun.info(f"{name} -> ptag: {ptag}")
328
329 if notFound:
330 logCPGridRun.error("Some input datasets are not available in AMI, missing datasets are likely to fail on the grid:")
331 logCPGridRun.error(", ".join(notFound))
332 return False
333
334 return True
335
337 return not self._tarballRecreated and (self.args.recreateTar or not os.path.exists(self._tarfile) or self._filesChanged())
338
340 return os.path.exists(self._tarfile) or self._tarballRecreated
341
342 def outputDSFormatter(self, name, label):
343 if CPGridRun.isAtlasProductionFormat(name) and not label:
344 return self._outputDSFormatter(name)
345 else:
346 return self._customOutputDSFormatter(name, label)
347
348 def _outputDSFormatter(self, name):
349 '''
350 {group/user}.{username}.{prefix}.{DSID}.{format}.{tags}.{suffix}
351 '''
352 nameParser = CPGridRun.atlasProductionNameParser(name)
353 base = 'group' if self.args.groupProduction else 'user'
354 username = self.args.gridUsername
355 dsid = nameParser['DSID']
356 tags = '_'.join(nameParser['tags'])
357 fileFormat = nameParser['format']
358 base = 'group' if self.args.groupProduction else 'user'
359 prefix = self.args.prefix if self.args.prefix else nameParser['main'].split('_')[0] # Dynamically set the prefix, likely to be something like PhPy8Eg
360 suffix = self._suffixFormatter()
361
362 result = [base, username, prefix, dsid, fileFormat, tags, suffix]
363 return ".".join(filter(None, result))
364
365 def _customOutputDSFormatter(self, name, label):
366 '''
367 {group/user}.{username}.{prefix}.{main}.{suffix}
368 '''
369 parts = name.split('.')
370 base = 'group' if self.args.groupProduction else 'user'
371 username = self.args.gridUsername
372 main = label if label else parts[2]
373 main = f"{self.args.prefix}.{main}" if self.args.prefix else main
374 main = f"{main}.{self.args.suffix}" if self.args.suffix else main
375
376 result = [base, username, main]
377 return ".".join(filter(None, result))
378
380 if self.args.suffix:
381 return self.args.suffix
382 if self.args.testRun:
383 import uuid
384 return f"test_{uuid.uuid4().hex[:6]}"
385 else:
386 ''
387
388 def _filesChanged(self):
389 tarball_mtime = os.path.getmtime(self._tarfile) if os.path.exists(self._tarfile) else 0
390 buildDir = self._buildDir()
391 sourceDir = self._sourceDir()
392
393 # Check for changes in buildDir
394 for root, _, files in os.walk(buildDir):
395 for file in files:
396 file_path = os.path.join(root, file)
397 try:
398 if os.path.getmtime(file_path) > tarball_mtime:
399 logCPGridRun.info(f"File {file_path} is newer than the tarball.")
400 return True
401 except FileNotFoundError:
402 continue
403
404 # Check for changes in sourceDir
405 if sourceDir is None:
406 logCPGridRun.warning("Source directory is not detected, auto-compression is not performed. Use --recreateTar to update the submission")
407 return False
408 for root, _, files in os.walk(sourceDir):
409 for file in files:
410 file_path = os.path.join(root, file)
411 try:
412 if os.path.getmtime(file_path) > tarball_mtime:
413 logCPGridRun.info(f"File {file_path} is newer than the tarball.")
414 return True
415 except FileNotFoundError:
416 continue
417 return False
418
419 def _buildDir(self):
420 buildDir = os.environ["CMAKE_PREFIX_PATH"]
421 buildDir = os.path.dirname(buildDir.split(":")[0])
422 return buildDir
423
424 def _sourceDir(self):
425 cmakeCachePath = os.path.join(self._buildDir(), 'CMakeCache.txt')
426 sourceDir = None
427 if not os.path.exists(cmakeCachePath):
428 return sourceDir
429 with open(cmakeCachePath, 'r') as cmakeCache:
430 for line in cmakeCache:
431 if '_SOURCE_DIR:STATIC=' in line:
432 sourceDir = line.split('=')[1].strip()
433 break
434 return sourceDir
435
436 def execFormatter(self):
437 if not self.args.exec:
438 raise ValueError('No exec command provided, use --exec to specify the command to run on the grid')
439
440 # Check if the execution command starts with 'CPRun.py' or '-'
441 isCPRunDefault = self.args.exec.startswith('-') or self.args.exec.startswith('CPRun.py')
442 formatingClause = {
443 'input_list': 'in.txt',
444 'merge_output_files': len(self.args.output_files) == 1,
445 }
446 if not isCPRunDefault:
447 if self._isFirstRun: logCPGridRun.warning("Non-CPRun.py is detected, please ensure the exec string is formatted correctly. Exec string will not be automatically formatted.")
448 return f'"{self.args.exec}"'
449
450 # Parse the exec string using the parser to validate and extract known arguments
451 self._initRunscript()
452 runscriptArgs, unknownArgs = self._runscript.parser.parse_known_args(self.args.exec.split(' '))
453
454 # Throw error if unknownArgs contains any --args
455 unknown_flags = [arg for arg in unknownArgs if arg.startswith('--')]
456 if unknown_flags:
457 logCPGridRun.error(f"Unknown flags detected in the exec string: {unknown_flags}. Please check the exec string.")
458 raise ValueError(f"Unknown arguments detected: {unknown_flags}")
459
460 # Only override if value is None or the parser default
461 for key, value in formatingClause.items():
462 if hasattr(runscriptArgs, key):
463 old_value = getattr(runscriptArgs, key)
464 if old_value is None or old_value == self._runscript.parser.get_default(key):
465 setattr(runscriptArgs, key, value)
466 if self._isFirstRun: logCPGridRun.info(f"Setting '{key}' to '{value}' (CPRun.py default is: '{old_value}')")
467 else:
468 if self._isFirstRun: logCPGridRun.warning(f"Preserving user-defined '{key}': '{old_value}', default formatting '{value}' will not be applied.")
469 else:
470 logCPGridRun.error(f"Formatting clause '{key}' is not recognized in the CPRun.py script. Check CPGridRun.py")
471 raise ValueError(f"Formatting clause '{key}' is not recognized in the CPRun.py script. Check CPGridRun.py")
472 self._checkYamlExists(runscriptArgs)
473 # Return the formatted arguments as a string
474 arg_string = ' '.join(
475 f'--{k.replace("_", "-")}' if isinstance(v, bool) and v else
476 f'--{k.replace("_", "-")} {v}' for k, v in vars(runscriptArgs).items() if v not in [None, False]
477 )
478 return f'"CPRun.py {arg_string}"'
479
480 def _checkYamlExists(self, runscriptArgs):
481 from AnalysisAlgorithmsConfig.CPBaseRunner import CPBaseRunner
482 if not hasattr(runscriptArgs, 'text_config'):
483 self._errorCollector['no yaml'] = "No YAML configuration file is specified in the exec string. Please provide one using --text-config"
484 return
485 yamlPath = getattr(runscriptArgs, 'text_config')
486 self._yamlPath = yamlPath
487 haveLocalYaml = CPBaseRunner.findLocalPathYamlConfig(yamlPath)
488 if haveLocalYaml:
489 logCPGridRun.warning("A path to a local YAML configuration file is found, but it may not be grid-usable.")
490
491 repoYamls, _ = CPBaseRunner.findRepoPathYamlConfig(yamlPath)
492 if repoYamls and len(repoYamls) > 1:
493 self._errorCollector['ambiguous yamls'] = f'Multiple files named \"{yamlPath}\" found in the analysis repository. Please provide a more specific path to the config file.\nMatches found:\n' + '\n'.join(repoYamls)
494 return
495 elif repoYamls and len(repoYamls) == 1:
496 logCPGridRun.info(f"Found a grid-usable YAML configuration file in the analysis repository: {repoYamls[0]}")
497 return
498
499 if haveLocalYaml and self.args.useCentralPackage:
500 logCPGridRun.warning("A path to a local YAML configuration file is found, no custom packages are found, proceed with /cvmfs packages only.")
501
502 if not repoYamls and not self.args.useCentralPackage:
503 self._errorCollector['no usable yaml'] = f"Grid usable YAML configuration file not found: {yamlPath}"
504 if haveLocalYaml:
505 self._errorCollector['have local yaml'] = f"Only a local YAML configuration file is found: {yamlPath}, not usable in the grid.\n" \
506 f"Make sure the YAML file is in build/x86_64-el9-gcc14-opt/data/package_name/config.yaml. You can install the YAML file through CMakeList.txt with `atlas_install_data( data/* )`; use `-t package_name/config.yaml` in the --exec\n"\
507 f"Or if you are only using central packages, please use the `--useCentralPackage` flag."
508
510 outputs = [f'{output.split(".")[0]}:{output}' if ":" not in output else output for output in self.args.output_files]
511 return ','.join(outputs)
512
513 def hasPrun(self) -> bool:
514 import shutil
515 prun_path = shutil.which("prun")
516 if prun_path is None:
517 self._errorCollector['no prun'] = (
518 "The 'prun' command is not found. If you are on lxplus, please run the following commands:\n\n"
519 "```\n"
520 "lsetup panda\n"
521 "voms-proxy-init -voms atlas\n"
522 "```\n"
523 "Make sure you have a valid certificate."
524 )
525 return False
526 return True
527
528 def submit(self):
529 import subprocess
530 for key, cmd in self.cmd.items():
531 logCPGridRun.info(f"Submitting: {self.outputs[key]}")
532 process = subprocess.Popen(cmd, shell=True, stdout=sys.stdout, stderr=sys.stderr)
533 process.communicate()
534
535 @staticmethod
537 if ":" in name:
538 name = name.split(":")[1]
539
540 if name.startswith("mc") or name.startswith("data"):
541 return True
542
543 logCPGridRun.warning("Name is not in the Atlas production format, assuming it is a user production")
544 return False
545
546 @staticmethod
548 '''
549 The custom name has many variations, but most of them follow user/group.username.datasetname.suffix
550 '''
551 result = {}
552 parts = filename.split('.')
553 result['userType'] = parts[0]
554 result['username'] = parts[1]
555 result['main'] = parts[2]
556 result['suffix'] = parts[-1]
557 return result
558
559 @staticmethod
561 '''
562 Parsing file name into a dictionary, an example is given here
563 mc20_13TeV.410470.PhPy8EG_A14_ttbar_hdamp258p75_nonallhad.deriv.DAOD_PHYS.e6337_s3681_r13167_p5855/DAOD_PHYS.34865530._000740.pool.root.1
564 For the first part
565 datasetName: mc20_13TeV.410470.PhPy8EG_A14_ttbar_hdamp258p75_nonallhad.deriv.DAOD_PHYS.e6337_s3681_r13167_p5855
566 projectName: mc20_13TeV
567 campaign: mc20
568 energy: 13 #(TeV)
569 DSID: 410470
570 main: PhPy8EG_A14_ttbar_hdamp258p75_nonallhad
571 TODO generator: PhPy8Eg
572 TODO tune: A14 # For Pythia8
573 TODO process: ttbar
574 TODO hdamp: 258p75 # For Powheg
575 TODO decayType: nonallhad
576 step: deriv
577 format: DAOD_PHYS
578 tags: e###_s###_r###_p###_a###_t###_b#
579 etag: e6337 # EVNT (EVGEN) production and merging
580 stag: s3681 # Geant4 simulation to produce HITS and merging!
581 rtag: r13167 # Digitisation and reconstruction, as well as AOD merging
582 ptag: p5855 # Production of NTUP_PILEUP format and merging
583 atag: aXXX: atlfast configuration (both simulation and digit/recon)
584 ttag: tXXX: tag production configuration
585 btag: bXXX: bytestream production configuration
586
587 For the second part
588 JeditaskID: 34865530
589 fileNumber: 000740
590 version: 1
591
592 '''
593 result = {}
594 #split the / in case
595 # mc20_13TeV.410470.PhPy8EG_A14_ttbar_hdamp258p75_nonallhad.deriv.DAOD_PHYS.e6337_s3681_r13167_p5855
596 # /DAOD_PHYS.34865530._000740.pool.root.1
597 if '/' in filename:
598 datasetPart, filePart = filename.split('/')
599 else:
600 datasetPart = filename
601 filePart = None
602
603 # Remove the scope
604 if ':' in datasetPart:
605 datasetPart = datasetPart.split(':')[1]
606
607 # Do not try to parse user datasets
608 if datasetPart.startswith('user') or datasetPart.startswith('group'):
609 result['datasetName'] = datasetPart
610 return result
611
612 # Split the dataset part by dots
613 datasetParts = datasetPart.split('.')
614 result['datasetName'] = datasetPart
615 # Extract the first part
616 result['projectName'] = datasetParts[0] # is positional
617 # Extract the campaign and energy
618 campaign_energy = result['projectName'].split('_')
619 result['campaign'] = campaign_energy[0]
620 result['energy'] = campaign_energy[1]
621
622 # Extract the DSID, positional
623 result['DSID'] = datasetParts[1]
624 result['main'] = datasetParts[2]
625 result['step'] = datasetParts[3]
626 result['format'] = datasetParts[4]
627
628 # Extract the tags (etag, stag, rtag, ptag)
629 tags = datasetParts[5].split('_')
630 result['tags'] = tags
631 for tag in tags:
632 if tag.startswith('e'):
633 result['etag'] = tag
634 elif tag.startswith('s'):
635 result['stag'] = tag
636 elif tag.startswith('r'):
637 result['rtag'] = tag
638 elif tag.startswith('p'):
639 result['ptag'] = tag
640 elif tag.startswith('a'):
641 result['atag'] = tag
642 elif tag.startswith('t'):
643 result['ttag'] = tag
644 elif tag.startswith('b'):
645 result['btag'] = tag
646
647 # Extract the file part if it exists
648 if filePart:
649 fileParts = filePart.split('.')
650 result['jediTaskID'] = fileParts[1]
651 result['fileNumber'] = fileParts[2]
652 result['version'] = fileParts[-1]
653 return result
654
655 @staticmethod
656 def _parseInputFileList(path: Path, bulk_submission: bool = False) -> tuple[list[str], list[str]]:
657 files = []
658 with path.open('r') as inputText:
659 for line in inputText.readlines():
660 # skip comments and empty lines
661 if line.startswith("#") or not line.strip():
662 continue
663 files += line.split(",")
664 # remove leading/trailing whitespaces, and \n
665 files = [file.strip() for file in files]
666
667 # bulk submission
668 if bulk_submission:
669 if any((path.parent / file).exists() or (path.parent / f"{file}.txt").exists() for file in files):
670 files_bulk = []
671 names_bulk = []
672 for file in files:
673 file_path = path.parent / file
674 if not file_path.exists():
675 file_path = path.parent / f"{file}.txt"
676 if not file_path.exists():
677 logCPGridRun.error(f"File {file} or {file}.txt does not exist in the input list directory.")
678 raise FileNotFoundError(f"File {file} or {file}.txt does not exist in the input list directory.")
679 files_current, names_current = CPGridRun._parseInputFileList(file_path, bulk_submission=True)
680 files_bulk.extend(files_current)
681 names_bulk.extend(names_current)
682 return files_bulk, names_bulk
683 else:
684 return [','.join(files)], [path.stem.replace("+", "")]
685
686 return files, [None]
687
689 if self._errorCollector:
690 logCPGridRun.error("Errors were collected during the script execution:")
691
692 for key, value in self._errorCollector.items():
693 logCPGridRun.error(f"{key}: {value}")
694 logCPGridRun.error("Please fix the errors and try again.")
695 sys.exit(1)
696
698 self.hasPrun()
699 if self.args.checkInputDS:
700 self.checkInputInPyami()
701
702 def askSubmission(self):
703 if self.args.agreeAll:
704 logCPGridRun.info("You have agreed to all the submission details. Jobs will be submitted without confirmation.")
705 self.submit()
706 return
707 answer = input("Please confirm ALL the submission details are correct before submitting [y/n]: ")
708 if answer.lower() == 'y':
709 self.submit()
710 elif answer.lower() == 'n':
711 logCPGridRun.info("Feel free to report any unexpected behavior to the CPAlgorithms team!")
712 else:
713 logCPGridRun.error("Invalid input. Please enter 'y' or 'n'. Jobs are not submitted.")
714
715if __name__ == '__main__':
716 cpgrid = CPGridRun()
717 cpgrid.configureSubmission()
718 cpgrid.printInputDetails()
719 cpgrid.checkExternalTools()
720 cpgrid.printDelayedErrorCollection()
721 cpgrid.askSubmission()
void printHelp()
void print(char *figname, TCanvas *c1)
dict _createPrunArgsDict(self)
Definition CPGridRun.py:92
outputDSFormatter(self, name, label)
Definition CPGridRun.py:342
rucioCustomNameParser(filename)
Definition CPGridRun.py:547
bool hasPrun(self)
Definition CPGridRun.py:513
_checkYamlExists(self, runscriptArgs)
Definition CPGridRun.py:480
_parseGridArguments(self)
Definition CPGridRun.py:45
bool checkInputInPyami(self)
Definition CPGridRun.py:269
bool _analyzeAmiResults(self, results, datasetPtag)
Definition CPGridRun.py:301
isAtlasProductionFormat(name)
Definition CPGridRun.py:536
tuple[list[str], list[str]] _parseInputFileList(Path path, bool bulk_submission=False)
Definition CPGridRun.py:656
_prepareAmiQueryFromInputList(self)
Definition CPGridRun.py:286
configureSubmissionSingleSample(self, input, name)
Definition CPGridRun.py:153
_filesChangedOrTarballNotCreated(self)
Definition CPGridRun.py:336
tuple[list[str], list[str]] _inputNames
Definition CPGridRun.py:21
_hasCompressedTarball(self)
Definition CPGridRun.py:339
atlasProductionNameParser(filename)
Definition CPGridRun.py:560
_outputDSFormatter(self, name)
Definition CPGridRun.py:348
_checkPrunArgs(self, argDict)
Definition CPGridRun.py:223
printDelayedErrorCollection(self)
Definition CPGridRun.py:688
_customOutputDSFormatter(self, name, label)
Definition CPGridRun.py:365
dict _unknownArgsDict(self)
Definition CPGridRun.py:207
bool exists(const std::string &filename)
does a file exist
std::vector< std::string > split(const std::string &s, const std::string &t=":")
Definition hcg.cxx:179