"""
Common command-line interface for Desmond scripts
Copyright Schrodinger, LLC. All rights reserved.
"""
import contextlib
import copy
import os
from argparse import * # noqa F403
from typing import List
from schrodinger import structure
from schrodinger.application.desmond import cms
from schrodinger.application.desmond.packages import topo
from schrodinger.application.desmond.packages import traj
from schrodinger.infra import mm
from schrodinger.job import jobcontrol
from schrodinger.job import launchapi
from schrodinger.structutils import analyze
from schrodinger.utils import cmdline
def _pretty_print(message, prefix=None):
if prefix:
indent = " " * len(prefix)
lines = message.split("\n")
print(f"{prefix}{lines[0]}")
for line in lines[1:]:
print(f"{indent}{line}")
else:
print(message)
[docs]def info(message):
_pretty_print(message, prefix="INFO: ")
[docs]def warn(message):
_pretty_print(message, prefix="WARNING: ")
[docs]def error(message, exit_code=1, should_exit=True):
_pretty_print(message, prefix="ERROR: ")
should_exit and exit(exit_code)
#
# Jobcontrol stuff
#
[docs]def all_fnames_in_dir(dname):
"""
Returns all regular files under the given directory, including those in
subdirectories.
:type dname: `str`
:param dname: Path of the directory
:rtype: `list` of `str`
"""
all_fnames = []
for dir, _, files in os.walk(dname):
all_fnames.extend([os.path.join(dir, e) for e in files])
return all_fnames
[docs]def jc_reg_output(builder, fname):
"""
Preregister an output file for jobcontrol to transfer.
:type builder: `launchapi.JobSpecificationArgsBuilder`
:param builder: You can get a builder by calling
`CommandLine.get_job_spec`.
:type fname: `str`
:param fname: Input file name to be transferred by jobcontrol. The file
can be a directory.
"""
builder.setOutputFile(runtime_path=fname)
[docs]def jc_backend_reg_output(fname):
"""
Register (on the fly) an output file for jobcontrol to transfer. No effects
if the process is not under jobcontrol.
:type fname: `str`
:param fname: Output file name to be transferred by jobcontrol. The file
can be a directory.
"""
jobbe = jobcontrol.get_backend()
if jobbe is not None:
jobbe.addOutputFile(fname)
#
# Command-line interface parsing
#
[docs]def parse_slice(s):
"""
Return a slice object derived from the given string `s`, in format of 'i',
'start:end', or 'start:end:step'.
"""
try:
a = [int(e) if e.strip() else None for e in s.split(":")]
if len(a) == 1: # treat single number as a slice with single index
a.append(a[0] + 1 or None)
return slice(*a)
except ValueError:
raise ArgumentTypeError("Invalid slice syntax: %s" % s)
def _parse_trj_frame_cutting_impl(s: str, num_frames: int) -> List[int]:
# `s` is the string of original frame-cutting (FC) expression, which may
# contain multiple space/comma-separated subexpressions.
def canonicalize_frame_index(index: str) -> int:
"""
Converts the given `index` to a nonnegative frame index number.
The returned number is guaranteed to be a valid index that is in the
(inclusive) range [0, `num_frames`].
This function raises `ValueError` if `index` cannot be converted to
`int`, or `IndexError` if the index is out of the range.
"""
orig_index = index
index = int(index)
if index < 0:
index = num_frames + index
if index < 0 or index >= num_frames:
raise IndexError(f"Frame index ({orig_index}) is out of range "
f"[0, {num_frames - 1}].")
return index
# First, let's break `s` into a number of non-empty strings, each of which
# represents an FC subexpression.
subexpressions = filter(
None, (sub.strip() for sub in s.replace(',', ' ').split()))
# Each subexpression is either a frame index or a frame range, we convert
# the string to the corresponding `int` or `int`s.
frame_indices = []
for sub in subexpressions:
try:
frame_indices.append(canonicalize_frame_index(sub))
except ValueError:
# `sub` should be a frame range (which is inclusive).
start, end, step = (sub.split(":") + [None])[:3]
start = canonicalize_frame_index(start) if start else 0
end = canonicalize_frame_index(end) if end else num_frames - 1
direction = (end > start) - (end < start)
step = int(step) if step else direction
if direction == 0:
raise SyntaxError(f"{sub} is a nil range")
if step == 0:
raise SyntaxError(f"Step in range cannot be zero: {sub}")
if direction > 0 and step < 0:
raise SyntaxError(f"Negative step in ascending range: {sub}")
if direction < 0 and step > 0:
raise SyntaxError(f"Positive step in descending range: {sub}")
frame_indices.extend(range(start, end + direction, step))
return frame_indices
def _parse_trj_frame_cutting(s: str, num_frames: int) -> List[int]:
"""
This parses the value of the -trj-frame-cutting option, and returns a list
of trajectory frame indices.
:param s: Raw string from the command line
:param num_frames: Number of frames in the trajectory
:raises: `ArgumentTypeError`, if parsing fails.
"""
try:
return _parse_trj_frame_cutting_impl(s, num_frames)
except Exception as e:
raise ArgumentTypeError("Parsing -trj-frame-cutting's value failed "
"with the following exception:\n"
" %s: %s\n" % (e.__class__.__name__, e))
def _cms_only(s):
# Note: only use this option if there is no need to interact with trajectory
try:
return cms.Cms(s), s
except Exception as e:
raise ArgumentTypeError(f"Reading input .cms file failed: {s}, {e}")
def _msys_cms(s):
try:
return topo.read_cms(s), s
except Exception as e:
raise ArgumentTypeError(f"Reading input .cms file failed: {s}, {e}")
def _read_mae(s):
try:
return structure.StructureReader.read(s), s
except Exception as e:
raise ArgumentTypeError(f"Reading structure file failed: {s}, {e}")
def _parse_trj(s):
if os.path.exists(s):
s = s.rstrip(os.sep)
else:
raise ArgumentTypeError(f"Trajectory does not exist: {s}")
try:
tr = traj.read_traj(s)
if not tr:
raise ArgumentTypeError(f"Trajectory is empty: {s}")
return tr, s
except Exception as e:
raise ArgumentTypeError(f"Reading trajectory failed: {s}, {e}")
def _parse_out(s):
if "" == s:
raise ArgumentTypeError("Output basename must NOT be an empty string")
return s
[docs]def validate_and_enclose_asl(value, allow_empty=False, allow_dynamic=True):
if value == '' and allow_empty:
return ''
if value in cms.Cms.META_ASL:
return value
if not analyze.validate_asl(value):
raise ArgumentTypeError(f"ASL not valid: {value}")
if not allow_dynamic:
mm.mmss_initialize(mm.error_handler)
if mm.mmasl_is_dynamic_expression(value):
raise ArgumentTypeError(f"Dynamic ASL is not allowed: {value}")
return f'({value})'
_TRJ_SYMBOL_TO_FMT = {
"auto": traj.Fmt.AUTO,
"dtr": traj.Fmt.DTR,
"xtc": traj.Fmt.XTC
}
def _parse_trj_fmt(s):
try:
return _TRJ_SYMBOL_TO_FMT[s]
except KeyError:
raise ArgumentTypeError("invalid choice: %s (choose from %s)" %
(s, ", ".join(_TRJ_SYMBOL_TO_FMT.keys())))
[docs]class LazyParser:
"""
Some argument parsers are so slow, e.g., the msys parser. We should avoid
running them unless they are really needed. For example, when user just
wants to see the help, or when there is another parsing errors which
interrupts the execution of the command, there is no need to run these
slow parsers.
Some arguments depend on the values of other arguments to be fully parsed,
e.g., -trj-frame-cutting (see its parser `_parse_trj_frame_cutting` above).
For both types of arguments, we can use this class to delay and order their
parsings.
"""
[docs] def __init__(self, parser, priority=0):
"""
:type parser: Callable object
:param parser: It should take a string argument, parse the string, and
return the resultant object.
:type priority: `int`: 0-9
:param priority: Specify the priority of parsing. High numbered object
will be parsed earlier.
"""
assert 0 <= priority < 10
self._parser = parser
self._string = None
self._priority = priority
[docs] def parse(self, *args, **kwargs):
"""
Calls the actual parser on the cached string and returns the parsing
result.
"""
# This method is to be called by `CommandLine._parse_args_impl'.
if self._string is not None:
return self._parser(self._string, *args, **kwargs)
@property
def string(self):
"""
Return the original string from the command line.
"""
return self._string
@property
def priority(self):
"""
Return the priority of this parser.
:rtype: `int`
"""
return self._priority
def __call__(self, string):
"""
Just caches the string to be parsed, and returns this `LazyParser`
object itself. The real parsing will be conducted when the `parse`
method of this class is explicitly called.
"""
self._string = string
return self
# yapf: disable
REQUIRE_CMS_ONLY = 1 << 0 # NOQA: E221
REQUIRE_MSYS_CMS = 1 << 1 # NOQA: E221
REQUIRE_TRJ = 1 << 2 # NOQA: E221
REQUIRE_OUT = 1 << 3 # NOQA: E221
OPTIONAL_TRJ = 1 << 4 # NOQA: E221
SLICE_TRJ = 1 << 5 # NOQA: E221
REF_MAE = 1 << 6 # NOQA: E221
REF_FRAME = 1 << 7 # NOQA: E221
JC = 1 << 8 # NOQA: E221
OUT_TRJ_FORMAT = 1 << 9 # NOQA: E221
TRJ_FRAME_CUTTING = 1 << 10 # NOQA: E221
# yapf: enable
[docs]class CommandLine(ArgumentParser):
"""
Use this class to define common command line arguments such input cms
file, input trajectory, output file base name, trajectory slicing option,
and so on.
After calling `parse_args` method, you will have those arguments parsed
properly, and you will get the `cms.Cms` object, the `msys.System` object,
and the trajectory object (sliced properly), automatically.
You can use the `spec` argument to custom your command line interface.
Examples:
1:
spec = REQUIRE_MSYS_CMS + REQUIRE_TRJ + REQUIRE_OUT + SLICE_TRJ
# This will give you the following:
# cms - First positional argument for cms input, and when it's parsed,
# you will get both msys and cms models.
# trj - Second positional argument for trajectory input
# out - Third positional argument for output base name
# -slice-trj
# - Option for slicing trajectory
2:
spec = REQUIRE_CMS_ONLY + REQUIRE_TRJ + REQUIRE_OUT + SLICE_TRJ
# Similar to the first example, but parsing the cms input file will return
# only the cms model.
3:
spec = REQUIRE_CMS_ONLY + OPTIONAL_TRJ + REQUIRE_OUT + SLICE_TRJ
# This will give you the following:
# cms - First positional argument for cms input, and when it's parsed,
# you will get the cms models.
# out - Second positional argument for output base name
# -trj - Option for trajectory input
# -slice-trj
# - Option for slicing trajectory
Other standard options:
-ref-mae Reference geometry from given .mae or .cms file. After parsing,
the `.ref_mae` attribute will provide
`(structure.Structure, str)`, where the first element is the
`Structure` object of the file and the second the file's name.
If this option is unspecified, the attribute' value will be
`None`.
spec: REF_MAE
-ref-frame
Reference geometry from a trajectory frame. After parsing, the
`.ref_frame` attribute will provide `(traj.Frame, int)`, where
the first element is the specified frame and the second the
index of the frame. If this option is overridden by -ref-mae, or
if the trajectory is not provided, the attribute's value will
be `None`.
spec: REF_FRAME
-output-trajectory-format
Allow user to specify the output trajectory format. After
parsing, the value of the `.out_trj_format` attribute will be
one of the contants defined within `traj.Fmt`.
spec: OUT_TRJ_FORMAT
-trj-frame-cutting
Allow user to edit the trajectory in a by-frame fashion. The
following are examples to demo the syntax of the value:
-trj-frame-cutting 0
# 1-frame trajectory containing only the 1st frame. Note that
# frame index is 0-based, which is consistent with the
# -slice-trj option.
-trj-frame-cutting 0,1,2
# 3-frame trajectory containing the initial three frames
-trj-frame-cutting "0 1 2"
# Both spaces and commas can be used as the delimiter. Multiple
# spaces or commas are allowed, and they are treated as a
# single space.
-trj-frame-cutting 0,0,1,2
# 4-frame trajectory where the first frame in the original
# trajectory is duplicated and inserted into the new trajectory
# as the second frame.
-trj-frame-cutting 2,1,0,0
# Similar to the above, but the frame order is reversed.
-trj-frame-cutting 0:5
# 6-frame trajectory containing initial six frames from the
# original trajectory. Note the range syntax. Note that the
# range is inclusive, which diverges from the Python syntax that
# -slice-trj uses, this is because the exclusive range is
# extremely error prone and difficult to use for selections
# though it's fine for slicing.
-trj-frame-cutting "0: 5"
# Invalid range expression: No space/commas allowed within a
# range expression. But if no trajectory specified, this option
# will be completely ignored, and no checking on the syntax will
# be done.
-trj-frame-cutting 0:-2
# Similar to the original trajectory, but with the last frame
# of the original trajectory dropped. Note the range syntax
# again.
-trj-frame-cutting ::
# A tricky way to select the whole original trajectory.
# Note the defaults:
# start = 0
# end = (number_of_frame - 1)
# step = 1
-trj-frame-cutting 0:10:2
# Note the range syntax again. `:2` specifies a step of 2.
# Selected frame indices: 0, 2, 4, 6, 8, 10.
-trj-frame-cutting 0:-1,0:-1
# Duplicate the original trajectory, and concatenate the two
# copies in a tail-to-head fashion.
-trj-frame-cutting -1:0
# Reverse the original trajectory
-trj-frame-cutting 0:4,6:-1
# Delete the frame 5.
"""
[docs] def __init__(self,
spec=(REQUIRE_MSYS_CMS + REQUIRE_TRJ + REQUIRE_OUT +
SLICE_TRJ),
**kwargs):
# Some specs are mutually exclusive.
assert not (REQUIRE_MSYS_CMS & spec and REQUIRE_CMS_ONLY & spec)
assert not (REQUIRE_TRJ & spec and OPTIONAL_TRJ & spec)
# Some specs have dependencies.
assert not (SLICE_TRJ & spec) or ((OPTIONAL_TRJ + REQUIRE_TRJ) & spec)
assert not (REF_FRAME & spec) or ((OPTIONAL_TRJ + REQUIRE_TRJ) & spec)
kwargs.setdefault("formatter_class", RawDescriptionHelpFormatter)
kwargs.setdefault("allow_abbrev", False)
ArgumentParser.__init__(self, **kwargs)
self._use_jc = JC & spec
if self._use_jc:
jc_opt = (kwargs.pop("jc_opt", None) or [
cmdline.HOST,
cmdline.JOBNAME,
cmdline.NOJOBID,
cmdline.WAIT,
cmdline.LOCAL,
cmdline.DEBUG,
cmdline.VIEWNAME,
cmdline.OPLSDIR,
])
cmdline.add_jobcontrol_options(self, jc_opt)
if REQUIRE_MSYS_CMS & spec:
self.add_argument("cms",
help="Input .cms file name",
type=LazyParser(_msys_cms))
if REQUIRE_CMS_ONLY & spec:
self.add_argument("cms",
help="Input .cms file name",
type=LazyParser(_cms_only))
if REQUIRE_TRJ & spec:
self.add_argument(
"trj",
help=
"Input trajectory file or dir name, which typically ends with"
" an extension name of '.xtc' (XTC format) or '_trj' (DTR format),"
" e.g., jobname.xtc, jobname_trj",
type=LazyParser(_parse_trj, 9))
if OPTIONAL_TRJ & spec:
self.add_argument(
"-t",
"-trj",
help=
"Input trajectory file or dir name, which typically ends with"
" an extension name of '.xtc' (XTC format) or '_trj' (DTR format),"
" e.g., jobname.xtc, jobname_trj",
dest="trj",
type=LazyParser(_parse_trj, 9))
if REQUIRE_OUT & spec:
self.add_argument("out",
help="Output file basename",
type=_parse_out)
if OUT_TRJ_FORMAT & spec:
self.add_argument(
"-output-trajectory-format",
metavar="{%s}" % ",".join(_TRJ_SYMBOL_TO_FMT.keys()),
type=_parse_trj_fmt,
dest="out_trj_format",
default=traj.Fmt.AUTO,
choices=_TRJ_SYMBOL_TO_FMT.values(),
help="Specify the output trajectory's format. Default: auto")
if SLICE_TRJ & spec:
self.add_argument("-s",
"-slice-trj",
metavar="START:END:STEP",
type=parse_slice,
dest="slice",
help="Use the sliced trajectory. We use "
"Python's slice notation. START, END, and "
"STEP should be integer numbers.")
if TRJ_FRAME_CUTTING & spec:
self.add_argument(
"-trj-frame-cutting",
metavar="FRAME-CUTTING",
type=LazyParser(_parse_trj_frame_cutting, 0),
help="Compose a new trajectory where the original trajectory "
"frames are selected and/or reordered using frame indices and "
"ranges. Note that frame indices are 0-based, and that frame "
"ranges are inclusive. For example: '0,0,1,3:5' will give a "
"new trajectory containing the following frames (in that "
"order) from the original trajectory: 0, 0, 1, 3, 4, 5. If the "
"'-slice-trj' option is also specified, -trj-frame-cutting "
"will work on the sliced trajectory; in other words, the "
"frame indices here are those of the sliced trajectory.")
if REF_MAE & spec:
self.add_argument(
"-ref-mae",
metavar="<file>",
help="Reference structure file in either .cms or .mae format",
type=LazyParser(_read_mae, 9))
if REF_FRAME & spec:
self.add_argument("-ref-frame",
metavar="frame-number",
type=int,
default=0,
help="Specify the frame to use as the reference "
"geometry. Frame indices are 0-based "
"indices. Number 0 is the first frame. "
"Negative numbers means counting from the "
"end of the trajectory, e.g., -1 means the "
"last frame. Default: 0")
[docs] def del_argument(self, flag=None, dest=None):
"""
Delete previously registered arguments that have the given flag or/and
`dest`. No effects if such arguments are not found.
Note: Positional arguments do NOT have flags. Specify `dest` to
identify the positional argument to delete.
:type flag: str
:type dest: str
"""
assert not (flag is None and dest is None)
actions_to_delete = []
for action in self._actions:
if ((flag is None or flag in action.option_strings) and
(dest is None or dest == action.dest)):
actions_to_delete.append(action)
for action in actions_to_delete:
c = action.container
c._remove_action(action)
for flag in action.option_strings:
# Completely deletes this option.
del c._option_string_actions[flag]
[docs] def edit_argument_help(self,
flag=None,
dest=None,
new_help="",
edit="replace"):
"""
Sometimes we want to use a standard option with a custom help message.
This method enables that.
Edit the help message of a previously registered argument as identified
by the `flag` or/and `dest` argument.
Note: Positional arguments do NOT have flags. Specify `dest` to
identify the positional argument to edit.
:type new_help: str, or `cui.SUPPRESS`
:param new_help: The custom help message. If it is `cui.SUPPRESS`, the
argument will NOT be shown in the help message.
:type edit: str. Must be one of the following: "replace", "prepend",
and "append" (case sensitive).
:param edit: Indicate how to edit the existing help message.
"replace" - Replace the old help message with `new_help`.
"append" - Append the old help message with `new_help`.
"prepend" - Prepend the old help message with `new_help`.
:raises: `KeyError`, if no argument is found.
"""
assert not (flag is None and dest is None)
assert edit in ["replace", "prepend", "append"]
for action in self._actions:
if ((flag is None or flag in action.option_strings) and
(dest is None or dest == action.dest)):
if edit == "replace":
action.help = new_help
elif edit == "prepend":
action.help = new_help + action.help
else:
action.help += new_help
break
else:
who = f"flag: {flag} " if flag else ""
who += f"dest: {dest} " if dest else ""
raise KeyError(f"{who}not found in arguments")
def _parse_args_impl(self, args, **kwargs):
a = ArgumentParser.parse_args(self, args, **kwargs)
if self._use_jc:
setattr(a, "jobname", os.environ.get("SCHRODINGER_JOBNAME"))
for k, v in vars(a).items():
if isinstance(v, LazyParser) and v.priority >= 5:
setattr(a, k, v.parse())
# - Unfortunately, the order of code matters here.
# This code should go before the one dealing with the `-slice-trj` and
# `-trj-frame-cutting`.
# - `-ref-mae' has precedence over `-ref-frame'.
try:
ref_mae = a.ref_mae
except AttributeError:
ref_mae = None
if ref_mae:
# When `-ref-mae is specified, force `ref_frame' to be None.
a.ref_frame = None
else:
try:
frame_index = a.ref_frame
except AttributeError:
frame_index = None
if frame_index is not None:
try:
tr, _ = a.trj
except (AttributeError, TypeError):
# See a comment below for why these two exceptions might
# be raised.
a.ref_frame = None
else:
try:
a.ref_frame = tr[frame_index], frame_index
except IndexError:
raise ArgumentTypeError("Invalid frame number: %d" %
frame_index)
# Slices the trajectory if necessary.
try:
slice = a.slice
tr, s = a.trj
except (AttributeError, TypeError):
# AttributeError: When Arguments don't include either `-slice-trj',
# or `trj', or both.
# TypeError: When `trj' option not specified, `a.trj' will be
# None, and the attempt to assign it to two
# variables will raise a `TypeError'.
pass
else:
# We have the `-slice-trj` option. It may not be given a value though.
if slice:
tr = tr[slice]
if not tr:
raise ArgumentTypeError(
f"-slice-trj results in an empty trajectory: {s}")
a.trj = tr, s
# Cuts the trajectory if necessary.
try:
tr, s = a.trj
frame_indices = a.trj_frame_cutting.parse(len(tr))
except (AttributeError, TypeError):
# See the comment above for `slice`.
# If we cannot parse `a.trj_frame_cutting`'s value, we'd better set
# it to `None` to avoid confusion.
with contextlib.suppress(AttributeError):
a.trj_frame_cutting = None
else:
# We have the `-trj-frame-cutting' option.
# The parser of this option guarantees that `frame_indices` is not
# an empty list.
# Right now, `a.trj_name_cutting` is still `LazyParser`, resets it
# to the actually parsing result to avoid confusion.
a.trj_frame_cutting = frame_indices
# Since each frame in the trajectory is guaranteed to be an
# independent object, a naive implementation like the following is
# wrong:
#
# new_tr = []
# for i in frame_indices:
# new_tr.append(tr[i])
#
new_tr = []
included_indices = set()
for i in frame_indices:
if i in included_indices:
new_tr.append(copy.deepcopy(tr[i]))
else:
new_tr.append(tr[i])
included_indices.add(i)
tr = new_tr
if not tr:
# We should guarantee `-trj-frame-cutting` does NOT result in
# any empty trajectory. If that happened, we must raise an
# exception.
raise ArgumentTypeError(
f"-trj-frame-cutting results in an empty trajectory: {s}")
a.trj = tr, s
# Finishes all delayed parsings.
for k, v in vars(a).items():
if isinstance(v, LazyParser) and v.priority < 5:
setattr(a, k, v.parse())
return a
[docs] def parse_args(self, args=None, **kwargs):
# Real work is done within `_parse_args_impl`. Here we only deal with
# exceptions.
try:
return self._parse_args_impl(args, **kwargs)
except ArgumentTypeError as e:
self.error(e)
[docs] def get_job_spec(self, args=None, **kwargs):
"""
Usage example:
In the python script::
def _get_cml():
cml = cui.CommandLine(spec=(cui.REQUIRE_MSYS_CMS + \
cui.REQUIRE_TRJ + cui.SLICE_TRJ + cui.REQUIRE_OUT + cui.JC),
description="my script")
cml.add_argument(...)
# Other application-specific options.
return cml
def get_job_spec_from_args(argv):
return _get_cml().get_job_spec(argv)[0]
The standard in-/output file options will be automatically registered for
jobcontrol to transfer. If you have non-standard in-/output files, you can
register them by yourself, for example::
def get_job_spec_from_args(argv):
_, builder, a = _get_cml().get_job_spec(argv)
cui.jc_reg_input(builder, a["input"])
cui.jc_reg_input(builder, "data/auxiliary_file_in_a_dir")
cui.jc_reg_output(builder, a["another_output"])
return builder.getJobSpec()
:rtype: (`launchapi.JobSpecification`,
`launchapi.JobSpecificationArgsBuilder`,
`dict`)
"""
# We do a shallow parsing and then figure out in-/output files.
arg_dict = vars(ArgumentParser.parse_args(self, args[1:], **kwargs))
for k, v in arg_dict.items():
if isinstance(v, LazyParser):
arg_dict[k] = v.string
builder = launchapi.JobSpecificationArgsBuilder(args,
use_jobname_log=True)
if arg_dict.get("cms"):
builder.setInputFile(arg_dict["cms"])
if arg_dict.get("trj"):
jc_reg_input(builder, arg_dict["trj"])
if arg_dict.get("ref_mae"):
builder.setInputFile(arg_dict["ref_mae"])
if arg_dict.get("out"):
builder.setOutputFile(arg_dict["out"])
return builder.getJobSpec(), builder, arg_dict