#!/usr/bin/env python """ The fMRIPrep on Docker wrapper This is a lightweight Python wrapper to run fMRIPrep. Docker must be installed and running. This can be checked running :: docker info Please report any feedback to our GitHub repository (https://github.com/poldracklab/fmriprep) and do not forget to credit all the authors of software that fMRIPrep uses (https://fmriprep.readthedocs.io/en/latest/citing.html). """ import sys import os import re import subprocess __version__ = '99.99.99' __copyright__ = 'Copyright 2020, Center for Reproducible Neuroscience, Stanford University' __credits__ = ['Craig Moodie', 'Ross Blair', 'Oscar Esteban', 'Chris Gorgolewski', 'Shoshana Berleant', 'Christopher J. Markiewicz', 'Russell A. Poldrack'] __bugreports__ = 'https://github.com/poldracklab/fmriprep/issues' MISSING = """ Image '{}' is missing Would you like to download? [Y/n] """ PKG_PATH = '/usr/local/miniconda/lib/python3.7/site-packages' TF_TEMPLATES = ( 'MNI152Lin', 'MNI152NLin2009cAsym', 'MNI152NLin6Asym', 'MNI152NLin6Sym', 'MNIInfant', 'MNIPediatricAsym', 'NKI', 'OASIS30ANTs', 'PNC', 'UNCInfant', 'fsLR', 'fsaverage', 'fsaverage5', 'fsaverage6', ) NONSTANDARD_REFERENCES = ( 'anat', 'T1w', 'run', 'func', 'sbref', 'fsnative' ) # Monkey-patch Py2 subprocess if not hasattr(subprocess, 'DEVNULL'): subprocess.DEVNULL = -3 if not hasattr(subprocess, 'run'): # Reimplement minimal functionality for usage in this file def _run(args, stdout=None, stderr=None): from collections import namedtuple result = namedtuple('CompletedProcess', 'stdout stderr returncode') devnull = None if subprocess.DEVNULL in (stdout, stderr): devnull = open(os.devnull, 'r+') if stdout == subprocess.DEVNULL: stdout = devnull if stderr == subprocess.DEVNULL: stderr = devnull proc = subprocess.Popen(args, stdout=stdout, stderr=stderr) stdout, stderr = proc.communicate() res = result(stdout, stderr, proc.returncode) if devnull is not None: devnull.close() return res subprocess.run = _run # De-fang Python 2's input - we don't eval user input try: input = raw_input except NameError: pass def check_docker(): """Verify that docker is installed and the user has permission to run docker images. Returns ------- -1 Docker can't be found 0 Docker found, but user can't connect to daemon 1 Test run OK """ try: ret = subprocess.run(['docker', 'version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: from errno import ENOENT if e.errno == ENOENT: return -1 raise e if ret.stderr.startswith(b"Cannot connect to the Docker daemon."): return 0 return 1 def check_image(image): """Check whether image is present on local system""" ret = subprocess.run(['docker', 'images', '-q', image], stdout=subprocess.PIPE) return bool(ret.stdout) def check_memory(image): """Check total memory from within a docker container""" ret = subprocess.run(['docker', 'run', '--rm', '--entrypoint=free', image, '-m'], stdout=subprocess.PIPE) if ret.returncode: return -1 mem = [line.decode().split()[1] for line in ret.stdout.splitlines() if line.startswith(b'Mem:')][0] return int(mem) def merge_help(wrapper_help, target_help): # Matches all flags with up to one nested square bracket opt_re = re.compile(r'(\[--?[\w-]+(?:[^\[\]]+(?:\[[^\[\]]+\])?)?\])') # Matches flag name only flag_re = re.compile(r'\[--?([\w-]+)[ \]]') # Normalize to Unix-style line breaks w_help = wrapper_help.rstrip().replace('\r', '') t_help = target_help.rstrip().replace('\r', '') w_usage, w_details = w_help.split('\n\n', 1) w_groups = w_details.split('\n\n') t_usage, t_details = t_help.split('\n\n', 1) t_groups = t_details.split('\n\n') w_posargs = w_usage.split('\n')[-1].lstrip() t_posargs = t_usage.split('\n')[-1].lstrip() w_options = opt_re.findall(w_usage) w_flags = sum(map(flag_re.findall, w_options), []) t_options = opt_re.findall(t_usage) t_flags = sum(map(flag_re.findall, t_options), []) # The following code makes this assumption assert w_flags[:2] == ['h', 'version'] assert w_posargs.replace(']', '').replace('[', '') == t_posargs # Make sure we're not clobbering options we don't mean to overlap = set(w_flags).intersection(t_flags) expected_overlap = { 'anat-derivatives', 'fs-license-file', 'fs-subjects-dir', 'h', 'use-plugin', 'version', 'w', } assert overlap == expected_overlap, "Clobbering options: {}".format( ', '.join(overlap - expected_overlap)) sections = [] # Construct usage start = w_usage[:w_usage.index(' [')] indent = ' ' * len(start) new_options = sum(( w_options[:2], [opt for opt, flag in zip(t_options, t_flags) if flag not in overlap], w_options[2:] ), []) opt_line_length = 79 - len(start) length = 0 opt_lines = [start] for opt in new_options: opt = ' ' + opt olen = len(opt) if length + olen <= opt_line_length: opt_lines[-1] += opt length += olen else: opt_lines.append(indent + opt) length = olen opt_lines.append(indent + ' ' + t_posargs) sections.append('\n'.join(opt_lines)) # Use target description and positional args sections.extend(t_groups[:2]) for line in t_groups[2].split('\n')[1:]: content = line.lstrip().split(',', 1)[0] if content[1:] not in overlap: w_groups[2] += '\n' + line sections.append(w_groups[2]) # All remaining sections, show target then wrapper (skipping duplicates) sections.extend(t_groups[3:] + w_groups[6:]) return '\n\n'.join(sections) def is_in_directory(filepath, directory): return os.path.realpath(filepath).startswith( os.path.realpath(directory) + os.sep) def get_parser(): """Defines the command line interface of the wrapper""" import argparse class ToDict(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): d = {} for kv in values: k, v = kv.split("=") d[k] = os.path.abspath(v) setattr(namespace, self.dest, d) parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=False) # Standard FMRIPREP arguments parser.add_argument('bids_dir', nargs='?', type=os.path.abspath, default='') parser.add_argument('output_dir', nargs='?', type=os.path.abspath, default='') parser.add_argument('analysis_level', nargs='?', choices=['participant'], default='participant') parser.add_argument('-h', '--help', action='store_true', help="show this help message and exit") parser.add_argument('--version', action='store_true', help="show program's version number and exit") # Allow alternative images (semi-developer) parser.add_argument('-i', '--image', metavar='IMG', type=str, default='poldracklab/fmriprep:{}'.format(__version__), help='image name') # Options for mapping files and directories into container # Update `expected_overlap` variable in merge_help() when adding to this g_wrap = parser.add_argument_group( 'Wrapper options', 'Standard options that require mapping files into the container') g_wrap.add_argument('-w', '--work-dir', action='store', type=os.path.abspath, help='path where intermediate results should be stored') g_wrap.add_argument( '--output-spaces', nargs="*", help="""\ Standard and non-standard spaces to resample anatomical and functional images to. \ Standard spaces may be specified by the form \ ``