Repository: nipy/heudiconv

Issue:
Issues w/ slurm
Greetings, 

heudiconv version 0.5.3. 

I am trying to run a basic heudiconv on one subject for testing w/ slurm (running on two nodes, partition name = heudiconv) to create my heuristic. 

1) Command as follows: 

heudiconv -d /srv/lab/fmri/mft/dicom4bids/data/{subject}*/* -f /srv/lab/fmri/mft/dicom4bids/data/convertall.py -s sub01 -q heudiconv -o /srv/lab/fmri/mft/dicom4bids/data/output/

This yields: 
INFO: Running heudiconv version 0.5.3
INFO: Need to process 1 study sessions
Traceback (most recent call last):
  File "/usr/local/anaconda3/envs/fmri/bin/heudiconv", line 11, in <module>
    sys.exit(main())
  File "/usr/local/anaconda3/envs/fmri/lib/python3.5/site-packages/heudiconv/cli/run.py", line 125, in main
    process_args(args)
  File "/usr/local/anaconda3/envs/fmri/lib/python3.5/site-packages/heudiconv/cli/run.py", line 296, in process_args
    study_outdir,
UnboundLocalError: local variable 'study_outdir' referenced before assignment

2) Now, adding "-l unknown" to the above command executes, but does not create a .heudiconv folder: 
INFO: Running heudiconv version 0.5.3
INFO: Need to process 1 study sessions
WARNING: Skipping unknown locator dataset

3) Curiously, removing the -q flag and the -l flag executes the command properly and creates .heudiconv under /output:
INFO: Running heudiconv version 0.5.3
INFO: Need to process 1 study sessions
INFO: PROCESSING STARTS: {'session': None, 'outdir': '/srv/lab/fmri/mft/dicom4bids/data/output/', 'subject': 'sub01'}
INFO: Processing 4976 dicoms
INFO: Analyzing 4976 dicoms
INFO: Generated sequence info with 10 entries
INFO: PROCESSING DONE: {'session': None, 'outdir': '/srv/lab/fmri/mft/dicom4bids/data/output/', 'subject': 'sub01'}

4) Anyone else experienced these issues? How come that adding -q breaks the locator inference? I still believe that a working scheduler integration for heudiconv will be extremely valuable w/ increasing data sets and I am happy to contribute. Perhaps a starting point would be to clarify what the -l flag actually requires. In the help menu for -l, it says: study path under outdir. If provided, it overloads the value provided by the heuristic. If --datalad is enabled, every directory within locator becomes a super-dataset thus establishing a hierarchy. Setting to "unknown" will skip that dataset. 

Thanks in advance!




Discussion:

Patch:
diff --git a/heudiconv/cli/run.py b/heudiconv/cli/run.py
index 0197912..0d984fc 100644
--- a/heudiconv/cli/run.py
+++ b/heudiconv/cli/run.py
@@ -1,3 +1,5 @@
+#!/usr/bin/env python
+
 import os
 import os.path as op
 from argparse import ArgumentParser
@@ -215,12 +217,11 @@ def get_parser():
     parser.add_argument('--dcmconfig', default=None,
                         help='JSON file for additional dcm2niix configuration')
     submission = parser.add_argument_group('Conversion submission options')
-    submission.add_argument('-q', '--queue', default=None,
-                            help='select batch system to submit jobs to instead'
-                                 ' of running the conversion serially')
-    submission.add_argument('--sbargs', dest='sbatch_args', default=None,
-                            help='Additional sbatch arguments if running with '
-                                 'queue arg')
+    submission.add_argument('-q', '--queue', choices=("SLURM", None),
+                            default=None,
+                            help='batch system to submit jobs in parallel')
+    submission.add_argument('--queue-args', dest='queue_args', default=None,
+                            help='Additional queue arguments')
     return parser
 
 
@@ -281,27 +282,28 @@ def process_args(args):
             continue
 
         if args.queue:
-            if seqinfo and not dicoms:
-                # flatten them all and provide into batching, which again
-                # would group them... heh
-                dicoms = sum(seqinfo.values(), [])
-                raise NotImplementedError(
-                    "we already grouped them so need to add a switch to avoid "
-                    "any grouping, so no outdir prefix doubled etc")
-
-            progname = op.abspath(inspect.getfile(inspect.currentframe()))
-
-            queue_conversion(progname,
+            # if seqinfo and not dicoms:
+            #     # flatten them all and provide into batching, which again
+            #     # would group them... heh
+            #     dicoms = sum(seqinfo.values(), [])
+            #     raise NotImplementedError(
+            #         "we already grouped them so need to add a switch to avoid "
+            #         "any grouping, so no outdir prefix doubled etc")
+
+            pyscript = op.abspath(inspect.getfile(inspect.currentframe()))
+
+            studyid = sid
+            if session:
+                studyid += "-%s" % session
+            if locator:
+                studyid += "-%s" % locator
+            # remove any separators
+            studyid = studyid.replace(op.sep, '_')
+
+            queue_conversion(pyscript,
                              args.queue,
-                             study_outdir,
-                             heuristic.filename,
-                             dicoms,
-                             sid,
-                             args.anon_cmd,
-                             args.converter,
-                             session,
-                             args.with_prov,
-                             args.bids)
+                             studyid,
+                             args.queue_args)
             continue
 
         anon_sid = anonymize_sid(sid, args.anon_cmd) if args.anon_cmd else None
diff --git a/heudiconv/dicoms.py b/heudiconv/dicoms.py
index 1206894..b94013f 100644
--- a/heudiconv/dicoms.py
+++ b/heudiconv/dicoms.py
@@ -4,7 +4,6 @@ import os.path as op
 import logging
 from collections import OrderedDict
 import tarfile
-from nibabel.nicom import csareader
 from heudiconv.external.pydicom import dcm
 
 from .utils import SeqInfo, load_json, set_readonly
diff --git a/heudiconv/parser.py b/heudiconv/parser.py
index 2ed0c04..8ceba64 100644
--- a/heudiconv/parser.py
+++ b/heudiconv/parser.py
@@ -186,7 +186,7 @@ def get_study_sessions(dicom_dir_template, files_opt, heuristic, outdir,
                                           "`infotoids` to heuristic file or "
                                           "provide `--subjects` option")
             lgr.warn("Heuristic is missing an `infotoids` method, assigning "
-                     "empty method and using provided subject id %s."
+                     "empty method and using provided subject id %s. "
                      "Provide `session` and `locator` fields for best results."
                      , sid)
             def infotoids(seqinfos, outdir):
diff --git a/heudiconv/queue.py b/heudiconv/queue.py
index 89c912f..ba8ad66 100644
--- a/heudiconv/queue.py
+++ b/heudiconv/queue.py
@@ -1,35 +1,62 @@
+import subprocess
+import sys
 import os
-import os.path as op
 
 import logging
 
 lgr = logging.getLogger(__name__)
 
-# start with SLURM but extend past that #TODO
-def queue_conversion(progname, queue, outdir, heuristic, dicoms, sid,
-                     anon_cmd, converter, session,with_prov, bids):
-
-        # Rework this...
-        convertcmd = ' '.join(['python', progname,
-                               '-o', outdir,
-                               '-f', heuristic,
-                               '-s', sid,
-                               '--anon-cmd', anon_cmd,
-                               '-c', converter])
-        if session:
-            convertcmd += " --ses '%s'" % session
-        if with_prov:
-            convertcmd += " --with-prov"
-        if bids:
-            convertcmd += " --bids"
-        if dicoms:
-            convertcmd += " --files"
-            convertcmd += [" '%s'" % f for f in dicoms]
-
-        script_file = 'dicom-%s.sh' % sid
-        with open(script_file, 'wt') as fp:
-            fp.writelines(['#!/bin/bash\n', convertcmd])
-        outcmd = 'sbatch -J dicom-%s -p %s -N1 -c2 --mem=20G %s' \
-                 % (sid, queue, script_file)
-
-        os.system(outcmd)
+def queue_conversion(pyscript, queue, studyid, queue_args=None):
+        """
+        Write out conversion arguments to file and submit to a job scheduler.
+        Parses `sys.argv` for heudiconv arguments.
+
+        Parameters
+        ----------
+        pyscript: file
+            path to `heudiconv` script
+        queue: string
+            batch scheduler to use
+        studyid: string
+            identifier for conversion
+        queue_args: string (optional)
+            additional queue arguments for job submission
+
+        Returns
+        -------
+        proc: int
+            Queue submission exit code
+        """
+
+        SUPPORTED_QUEUES = {'SLURM': 'sbatch'}
+        if queue not in SUPPORTED_QUEUES:
+            raise NotImplementedError("Queuing with %s is not supported", queue)
+
+        args = sys.argv[1:]
+        # search args for queue flag
+        for i, arg in enumerate(args):
+            if arg in ["-q", "--queue"]:
+                break
+        if i == len(args) - 1:
+            raise RuntimeError(
+                "Queue flag not found (must be provided as a command-line arg)"
+            )
+        # remove queue flag and value
+        del args[i:i+2]
+
+        # make arguments executable again
+        args.insert(0, pyscript)
+        pypath = sys.executable or "python"
+        args.insert(0, pypath)
+        convertcmd = " ".join(args)
+
+        # will overwrite across subjects
+        queue_file = os.path.abspath('heudiconv-%s.sh' % queue)
+        with open(queue_file, 'wt') as fp:
+            fp.writelines(['#!/bin/bash\n', convertcmd, '\n'])
+
+        cmd = [SUPPORTED_QUEUES[queue], queue_file]
+        if queue_args:
+            cmd.insert(1, queue_args)
+        proc = subprocess.call(cmd)
+        return proc


Test patch:
diff --git a/heudiconv/tests/test_queue.py b/heudiconv/tests/test_queue.py
new file mode 100644
index 0000000..a90dd9b
--- /dev/null
+++ b/heudiconv/tests/test_queue.py
@@ -0,0 +1,46 @@
+import os
+import sys
+import subprocess
+
+from heudiconv.cli.run import main as runner
+from .utils import TESTS_DATA_PATH
+import pytest
+from nipype.utils.filemanip import which
+
+@pytest.mark.skipif(which("sbatch"), reason="skip a real slurm call")
+@pytest.mark.parametrize(
+    'invocation', [
+        "--files %s/01-fmap_acq-3mm" % TESTS_DATA_PATH,    # our new way with automated groupping
+        "-d %s/{subject}/* -s 01-fmap_acq-3mm" % TESTS_DATA_PATH # "old" way specifying subject
+    ])
+def test_queue_no_slurm(tmpdir, invocation):
+    tmpdir.chdir()
+    hargs = invocation.split(" ")
+    hargs.extend(["-f", "reproin", "-b", "--minmeta", "--queue", "SLURM"])
+
+    # simulate command-line call
+    _sys_args = sys.argv
+    sys.argv = ['heudiconv'] + hargs
+
+    try:
+        with pytest.raises(OSError):
+            runner(hargs)
+        # should have generated a slurm submission script
+        slurm_cmd_file = (tmpdir / 'heudiconv-SLURM.sh').strpath
+        assert slurm_cmd_file
+        # check contents and ensure args match
+        with open(slurm_cmd_file) as fp:
+            lines = fp.readlines()
+        assert lines[0] == "#!/bin/bash\n"
+        cmd = lines[1]
+
+        # check that all flags we gave still being called
+        for arg in hargs:
+            # except --queue <queue>
+            if arg in ['--queue', 'SLURM']:
+                assert arg not in cmd
+            else:
+                assert arg in cmd
+    finally:
+        # revert before breaking something
+        sys.argv = _sys_args