4from AnaAlgorithm.DualUseConfig
import isAthena
5from AnaAlgorithm.Logging
import logging
10logCPGridRun = logging.getLogger(
'CPGridRun')
34 from AnalysisAlgorithmsConfig.AthenaCPRunScript
import AthenaCPRunScript
37 from AnalysisAlgorithmsConfig.EventLoopCPRunScript
import EventLoopCPRunScript
42 parser = argparse.ArgumentParser(description=
'CPGrid runscript to submit CPRun.py jobs to the grid. '
43 'This script will submit a job to the grid using files in the input text one by one.'
44 'CPRun.py can handle multiple sources of input and create one output; but not this script',
46 formatter_class=argparse.RawTextHelpFormatter)
47 parser.add_argument(
'-h',
'--help', dest=
'help', action=
'store_true', help=
'Show this help message and continue')
49 ioGroup = parser.add_argument_group(
'Input/Output file configuration')
50 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')
51 ioGroup.add_argument(
'--output-files', dest=
'output_files', nargs=
'+', default=[
'output.root'],
52 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')
53 ioGroup.add_argument(
'--destSE', dest=
'destSE', default=
'', type=str, help=
'Destination storage element (PanDA)')
54 ioGroup.add_argument(
'--mergeType', dest=
'mergeType', default=
'Default', type=str, help=
'Output merging type, [None, Default, xAOD]')
56 pandaGroup = parser.add_argument_group(
'Input/Output naming configuration')
57 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')
58 pandaGroup.add_argument(
'--prefix', dest=
'prefix', default=
'', type=str, help=
'Prefix for the output directory. Dynamically set with input container if not provided')
59 pandaGroup.add_argument(
'--suffix', dest=
'suffix', default=
'',type=str, help=
'Suffix for the output directory')
60 pandaGroup.add_argument(
'--outDS', dest=
'outDS', default=
'', type=str,
61 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')
63 cpgridGroup = parser.add_argument_group(
'CPGrid configuration')
64 cpgridGroup.add_argument(
'--groupProduction', dest=
'groupProduction', action=
'store_true', help=
'Only use for official production')
66 cpgridGroup.add_argument(
'--exec', dest=
'exec', type=str,
67 help=
'Executable line for the CPRun.py or custom script to run on the grid encapsulated in a double quote (PanDA)\n'
68 'Run CPRun.py with preset behavior including streamlined file i/o. E.g, "CPRun.py -t config.yaml --no-systematics".\n'
69 'Run custom script: "customRun.py -i inputs -o output --text-config config.yaml --flagA --flagB"\n'
72 submissionGroup = parser.add_argument_group(
'Submission configuration')
73 submissionGroup.add_argument(
'-y',
'--agreeAll', dest=
'agreeAll', action=
'store_true', help=
'Agree to all the submission details without asking for confirmation. Use with caution!')
74 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')
75 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)')
76 submissionGroup.add_argument(
'--checkInputDS', dest=
'checkInputDS', action=
'store_true', help=
'Check if the input datasets are available on the AMI.')
77 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')
78 submissionGroup.add_argument(
'--useCentralPackage', dest=
'useCentralPackage', action=
'store_true', help=
'Use central package instead of custom packages')
85 converting unknown args to a dictionary
88 if unknownArgsDict
and self.
hasPrun():
90 logCPGridRun.info(f
"Adding prun exclusive arguments: {unknownArgsDict.keys()}")
92 logCPGridRun.warning(f
"Unknown arguments detected: {unknownArgsDict}. Cannot check the availablility in Prun because Prun is not available / noSubmit is on.")
95 return unknownArgsDict
99 if not self.
args.input_list:
100 raise ValueError(
'No input list provided, use --input-list to specify the input containers')
102 if self.
args.input_list.endswith(
'.txt'):
103 self.
_inputList = CPGridRun._parseInputFileList(self.
args.input_list)
104 elif self.
args.input_list.endswith(
'.json'):
105 raise NotImplementedError(
'JSON input list parsing is not implemented')
106 elif CPGridRun.isAtlasProductionFormat(self.
args.input_list):
110 'use --input-list to specify input containers')
115 for output
in self.
args.output_files:
117 output_files.extend(output.split(
','))
119 output_files.append(output)
124 logCPGridRun.info(
"\033[92m\n If you are using CPRun.py, the following flags are for the CPRun.py in this framework\033[0m")
125 self.
_runscript.parser.usage = argparse.SUPPRESS
136 self.
cmd[input] = cmd
143 'cmtConfig': os.environ[
"CMTCONFIG"],
144 'writeInputToTxt':
'IN:in.txt',
148 'addNthFieldOfInDSToLFN':
'2,3,6',
150 if self.
args.noSubmit:
151 config[
'noSubmit'] =
True
153 if self.
args.mergeType ==
'xAOD':
154 config[
'mergeScript'] =
'xAODMerge %OUT `echo %IN | sed \'s/,/ /g\'`'
156 if self.
args.mergeType !=
'None':
157 config[
'mergeOutput'] =
True
160 if self.
args.useCentralPackage:
162 config[
'noBuild'] =
True
163 config[
'noCompile'] =
True
164 config[
'athenaTag'] = f
"AnalysisBase,{os.environ['AnalysisBase_VERSION']}"
166 config[
'outTarBall'] = self.
_tarfile
167 config[
'useAthenaPackages'] =
True
171 config[
'useAthenaPackages'] =
True
173 if self.
args.groupProduction:
174 config[
'official'] =
True
175 config[
'voms'] = f
'atlas:/atlas/{self.args.gridUsername}/Role=production'
178 config[
'destSE'] = self.
args.destSE
180 if self.
args.testRun:
181 config[
'nEventsPerFile'] = 100
185 for k, v
in config.items():
186 if isinstance(v, bool)
and v:
188 elif v
is not None and v !=
'':
189 cmd += f
'--{k} {v} \\\n'
190 return cmd.rstrip(
' \\\n')
194 Cleans the unknown args by removing leading dashes and ensuring they are in key-value pairs
196 unknown_args_dict = {}
204 unknown_args_dict[self.
unknown_args[idx].lstrip(
'-')] =
True
206 return unknown_args_dict
210 check the arguments against the prun script to ensure they are valid
211 See https://github.com/PanDAWMS/panda-client/blob/master/pandaclient/PrunScript.py
213 import pandaclient.PrunScript
215 original_argv = sys.argv
218 prunArgsDict = pandaclient.PrunScript.main(get_options=
True)
219 sys.argv = original_argv
220 nonPrunOrCPGridArgs = []
222 if arg
not in prunArgsDict:
223 nonPrunOrCPGridArgs.append(arg)
224 if nonPrunOrCPGridArgs:
225 logCPGridRun.error(f
"Unknown arguments detected: {nonPrunOrCPGridArgs}. They do not belong to CPGridRun or Panda.")
226 raise ValueError(f
"Unknown arguments detected: {nonPrunOrCPGridArgs}. They do not belong to CPGridRun or Panda.")
229 for key, cmd
in self.
cmd.items():
230 parsed_name = CPGridRun.atlasProductionNameParser(key)
231 logCPGridRun.info(
"\n"
233 "\n".join([f
" {k.replace('_', ' ').title()}: {v}" for k, v
in parsed_name.items()]))
234 logCPGridRun.info(f
"Command: \n{cmd}")
242 import pyAMI.atlas.api
243 except ModuleNotFoundError:
245 "Cannot import pyAMI, please run the following commands:\n\n"
248 "voms-proxy-init -voms atlas\n"
250 "and make sure you have a valid certificate.")
258 client = pyAMI.client.Client(
'atlas')
259 pyAMI.atlas.api.init()
263 results = pyAMI.atlas.api.list_datasets(client, patterns=queries)
264 except pyAMI.exception.Error:
266 "Cannot query AMI, please run 'voms-proxy-init -voms atlas' and ensure your certificate is valid.")
273 Helper function to prepare a list of queries for the AMI based on the input list.
274 It will replace the _p### with _p% to match the latest ptag.
277 regex = re.compile(
"_p[0-9]+")
280 for datasetName
in self.
cmd:
281 parsed = CPGridRun.atlasProductionNameParser(datasetName)
282 datasetPtag[datasetName] = parsed.get(
'ptag')
283 queries.append(regex.sub(
"_p%", datasetName))
284 return queries, datasetPtag
288 regex = re.compile(
"_p[0-9]+")
289 results = [r[
'ldn']
for r
in results]
293 for datasetName
in self.
cmd:
294 if datasetName
not in results:
295 notFound.append(datasetName)
297 base = regex.sub(
"_p%", datasetName)
298 matching = [r
for r
in results
if r.startswith(base.replace(
"_p%",
""))]
300 mParsed = CPGridRun.atlasProductionNameParser(m)
302 mPtagInt = int(mParsed.get(
'ptag',
'p0')[1:])
303 currentPtagInt = int(datasetPtag.get(datasetName,
'p0')[1:])
304 if mPtagInt > currentPtagInt:
305 latestPtag[datasetName] = f
"p{mPtagInt}"
306 except (ValueError, TypeError):
310 logCPGridRun.info(
"Newer version of datasets found in AMI:")
311 for name, ptag
in latestPtag.items():
312 logCPGridRun.info(f
"{name} -> ptag: {ptag}")
315 logCPGridRun.error(
"Some input datasets are not available in AMI, missing datasets are likely to fail on the grid:")
316 logCPGridRun.error(
", ".join(notFound))
328 if CPGridRun.isAtlasProductionFormat(name):
335 {group/user}.{username}.{prefix}.{DSID}.{format}.{tags}.{suffix}
337 nameParser = CPGridRun.atlasProductionNameParser(name)
338 base =
'group' if self.
args.groupProduction
else 'user'
339 username = self.
args.gridUsername
340 dsid = nameParser[
'DSID']
341 tags =
'_'.join(nameParser[
'tags'])
342 fileFormat = nameParser[
'format']
343 base =
'group' if self.
args.groupProduction
else 'user'
344 prefix = self.
args.prefix
if self.
args.prefix
else nameParser[
'main'].
split(
'_')[0]
347 result = [base, username, prefix, dsid, fileFormat, tags, suffix]
348 return ".".join(filter(
None, result))
352 {group/user}.{username}.{main}.outputDS.{suffix}
354 parts = name.split(
'.')
355 base =
'group' if self.
args.groupProduction
else 'user'
356 username = self.
args.gridUsername
358 outputDS =
'outputDS'
361 result = [base, username,main, outputDS, suffix]
362 return ".".join(filter(
None, result))
366 return self.
args.suffix
367 if self.
args.testRun:
369 return f
"test_{uuid.uuid4().hex[:6]}"
374 tarball_mtime = os.path.getmtime(self.
_tarfile)
if os.path.exists(self.
_tarfile)
else 0
379 for root, _, files
in os.walk(buildDir):
381 file_path = os.path.join(root, file)
383 if os.path.getmtime(file_path) > tarball_mtime:
384 logCPGridRun.info(f
"File {file_path} is newer than the tarball.")
386 except FileNotFoundError:
390 if sourceDir
is None:
391 logCPGridRun.warning(
"Source directory is not detected, auto-compression is not performed. Use --recreateTar to update the submission")
393 for root, _, files
in os.walk(sourceDir):
395 file_path = os.path.join(root, file)
397 if os.path.getmtime(file_path) > tarball_mtime:
398 logCPGridRun.info(f
"File {file_path} is newer than the tarball.")
400 except FileNotFoundError:
405 buildDir = os.environ[
"CMAKE_PREFIX_PATH"]
406 buildDir = os.path.dirname(buildDir.split(
":")[0])
410 cmakeCachePath = os.path.join(self.
_buildDir(),
'CMakeCache.txt')
412 if not os.path.exists(cmakeCachePath):
414 with open(cmakeCachePath,
'r')
as cmakeCache:
415 for line
in cmakeCache:
416 if '_SOURCE_DIR:STATIC=' in line:
417 sourceDir = line.split(
'=')[1].
strip()
422 if not self.
args.exec:
423 raise ValueError(
'No exec command provided, use --exec to specify the command to run on the grid')
426 isCPRunDefault = self.
args.exec.startswith(
'-')
or self.
args.exec.startswith(
'CPRun.py')
428 'input_list':
'in.txt',
429 'merge_output_files': len(self.
args.output_files) == 1,
431 if not isCPRunDefault:
432 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.")
433 return f
'"{self.args.exec}"'
437 runscriptArgs, unknownArgs = self.
_runscript.parser.parse_known_args(self.
args.exec.split(
' '))
440 unknown_flags = [arg
for arg
in unknownArgs
if arg.startswith(
'--')]
442 logCPGridRun.error(f
"Unknown flags detected in the exec string: {unknown_flags}. Please check the exec string.")
443 raise ValueError(f
"Unknown arguments detected: {unknown_flags}")
446 for key, value
in formatingClause.items():
447 if hasattr(runscriptArgs, key):
448 old_value = getattr(runscriptArgs, key)
449 if old_value
is None or old_value == self.
_runscript.parser.get_default(key):
450 setattr(runscriptArgs, key, value)
451 if self.
_isFirstRun: logCPGridRun.info(f
"Setting '{key}' to '{value}' (CPRun.py default is: '{old_value}')")
453 if self.
_isFirstRun: logCPGridRun.warning(f
"Preserving user-defined '{key}': '{old_value}', default formatting '{value}' will not be applied.")
455 logCPGridRun.error(f
"Formatting clause '{key}' is not recognized in the CPRun.py script. Check CPGridRun.py")
456 raise ValueError(f
"Formatting clause '{key}' is not recognized in the CPRun.py script. Check CPGridRun.py")
459 arg_string =
' '.join(
460 f
'--{k.replace("_", "-")}' if isinstance(v, bool)
and v
else
461 f
'--{k.replace("_", "-")} {v}' for k, v
in vars(runscriptArgs).items()
if v
not in [
None,
False]
463 return f
'"CPRun.py {arg_string}"'
466 from AnalysisAlgorithmsConfig.CPBaseRunner
import CPBaseRunner
467 if not hasattr(runscriptArgs,
'text_config'):
468 self.
_errorCollector[
'no yaml'] =
"No YAML configuration file is specified in the exec string. Please provide one using --text-config"
470 yamlPath = getattr(runscriptArgs,
'text_config')
472 haveLocalYaml = CPBaseRunner.findLocalPathYamlConfig(yamlPath)
474 logCPGridRun.warning(
"A path to a local YAML configuration file is found, but it may not be grid-usable.")
476 repoYamls, _ = CPBaseRunner.findRepoPathYamlConfig(yamlPath)
477 if repoYamls
and len(repoYamls) > 1:
478 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)
480 elif repoYamls
and len(repoYamls) == 1:
481 logCPGridRun.info(f
"Found a grid-usable YAML configuration file in the analysis repository: {repoYamls[0]}")
484 if haveLocalYaml
and self.
args.useCentralPackage:
485 logCPGridRun.warning(
"A path to a local YAML configuration file is found, no custom packages are found, proceed with /cvmfs packages only.")
487 if not repoYamls
and not self.
args.useCentralPackage:
488 self.
_errorCollector[
'no usable yaml'] = f
"Grid usable YAML configuration file not found: {yamlPath}"
490 self.
_errorCollector[
'have local yaml'] = f
"Only a local YAML configuration file is found: {yamlPath}, not usable in the grid.\n" \
491 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"\
492 f
"Or if you are only using central packages, please use the `--useCentralPackage` flag."
495 outputs = [f
'{output.split(".")[0]}:{output}' if ":" not in output
else output
for output
in self.
args.output_files]
496 return ','.join(outputs)
500 prun_path = shutil.which(
"prun")
501 if prun_path
is None:
503 "The 'prun' command is not found. If you are on lxplus, please run the following commands:\n\n"
506 "voms-proxy-init -voms atlas\n"
508 "Make sure you have a valid certificate."
515 for key, cmd
in self.
cmd.items():
516 process = subprocess.Popen(cmd, shell=
True, stdout=sys.stdout, stderr=sys.stderr)
517 process.communicate()
522 name = name.split(
":")[1]
524 if name.startswith(
'mc')
or name.startswith(
'data'):
527 logCPGridRun.warning(
"Name is not in the Atlas production format, assuming it is a user production")
533 The custom name has many variations, but most of them follow user/group.username.datasetname.suffix
536 parts = filename.split(
'.')
537 result[
'userType'] = parts[0]
538 result[
'username'] = parts[1]
539 result[
'main'] = parts[2]
540 result[
'suffix'] = parts[-1]
546 Parsing file name into a dictionary, an example is given here
547 mc20_13TeV.410470.PhPy8EG_A14_ttbar_hdamp258p75_nonallhad.deriv.DAOD_PHYS.e6337_s3681_r13167_p5855/DAOD_PHYS.34865530._000740.pool.root.1
549 datasetName: mc20_13TeV.410470.PhPy8EG_A14_ttbar_hdamp258p75_nonallhad.deriv.DAOD_PHYS.e6337_s3681_r13167_p5855
550 projectName: mc20_13TeV
554 main: PhPy8EG_A14_ttbar_hdamp258p75_nonallhad
555 TODO generator: PhPy8Eg
556 TODO tune: A14 # For Pythia8
558 TODO hdamp: 258p75 # For Powheg
559 TODO decayType: nonallhad
562 tags: e###_s###_r###_p###_a###_t###_b#
563 etag: e6337 # EVNT (EVGEN) production and merging
564 stag: s3681 # Geant4 simulation to produce HITS and merging!
565 rtag: r13167 # Digitisation and reconstruction, as well as AOD merging
566 ptag: p5855 # Production of NTUP_PILEUP format and merging
567 atag: aXXX: atlfast configuration (both simulation and digit/recon)
568 ttag: tXXX: tag production configuration
569 btag: bXXX: bytestream production configuration
582 datasetPart, filePart = filename.split(
'/')
584 datasetPart = filename
588 if ':' in datasetPart:
589 datasetPart = datasetPart.split(
':')[1]
592 if datasetPart.startswith(
'user')
or datasetPart.startswith(
'group'):
593 result[
'datasetName'] = datasetPart
597 datasetParts = datasetPart.split(
'.')
598 result[
'datasetName'] = datasetPart
600 result[
'projectName'] = datasetParts[0]
602 campaign_energy = result[
'projectName'].
split(
'_')
603 result[
'campaign'] = campaign_energy[0]
604 result[
'energy'] = campaign_energy[1]
607 result[
'DSID'] = datasetParts[1]
608 result[
'main'] = datasetParts[2]
609 result[
'step'] = datasetParts[3]
610 result[
'format'] = datasetParts[4]
613 tags = datasetParts[5].
split(
'_')
614 result[
'tags'] = tags
616 if tag.startswith(
'e'):
618 elif tag.startswith(
's'):
620 elif tag.startswith(
'r'):
622 elif tag.startswith(
'p'):
624 elif tag.startswith(
'a'):
626 elif tag.startswith(
't'):
628 elif tag.startswith(
'b'):
633 fileParts = filePart.split(
'.')
634 result[
'jediTaskID'] = fileParts[1]
635 result[
'fileNumber'] = fileParts[2]
636 result[
'version'] = fileParts[-1]
642 with open(path,
'r')
as inputText:
643 for line
in inputText.readlines():
645 if line.startswith(
'#')
or not line.strip():
647 files += line.split(
',')
649 files = [file.strip()
for file
in files]
654 logCPGridRun.error(
"Errors were collected during the script execution:")
657 logCPGridRun.error(f
"{key}: {value}")
658 logCPGridRun.error(
"Please fix the errors and try again.")
663 if self.
args.checkInputDS:
667 if self.
args.agreeAll:
668 logCPGridRun.info(
"You have agreed to all the submission details. Jobs will be submitted without confirmation.")
671 answer = input(
"Please confirm ALL the submission details are correct before submitting [y/n]: ")
672 if answer.lower() ==
'y':
674 elif answer.lower() ==
'n':
675 logCPGridRun.info(
"Feel free to report any unexpected behavior to the CPAlgorithms team!")
677 logCPGridRun.error(
"Invalid input. Please enter 'y' or 'n'. Jobs are not submitted.")
679if __name__ ==
'__main__':
681 cpgrid.configureSubmission()
682 cpgrid.printInputDetails()
683 cpgrid.checkExternalTools()
684 cpgrid.printDelayedErrorCollection()
685 cpgrid.askSubmission()
void print(char *figname, TCanvas *c1)
outputDSFormatter(self, name)
dict _createPrunArgsDict(self)
rucioCustomNameParser(filename)
_checkYamlExists(self, runscriptArgs)
_parseGridArguments(self)
bool checkInputInPyami(self)
bool _analyzeAmiResults(self, results, datasetPtag)
isAtlasProductionFormat(name)
_customOutputDSFormatter(self, name)
_prepareAmiQueryFromInputList(self)
_parseInputFileList(path)
_filesChangedOrTarballNotCreated(self)
_hasCompressedTarball(self)
atlasProductionNameParser(filename)
_outputDSFormatter(self, name)
_checkPrunArgs(self, argDict)
configureSubmission(self)
printDelayedErrorCollection(self)
dict _unknownArgsDict(self)
configureSubmissionSingleSample(self, input)
std::vector< std::string > split(const std::string &s, const std::string &t=":")