From e35cbf81de41881637df5bd421cd98b64057f76f Mon Sep 17 00:00:00 2001 From: Adam Richie-Halford Date: Wed, 12 Dec 2018 09:33:31 -0800 Subject: [PATCH 01/13] Add EddyQuad interface --- nipype/interfaces/fsl/__init__.py | 2 +- nipype/interfaces/fsl/epi.py | 214 ++++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+), 1 deletion(-) diff --git a/nipype/interfaces/fsl/__init__.py b/nipype/interfaces/fsl/__init__.py index e8f192f4f2..c6de303307 100644 --- a/nipype/interfaces/fsl/__init__.py +++ b/nipype/interfaces/fsl/__init__.py @@ -21,7 +21,7 @@ WarpPointsFromStd, RobustFOV, CopyGeom, MotionOutliers) from .epi import (PrepareFieldmap, TOPUP, ApplyTOPUP, Eddy, EPIDeWarp, SigLoss, - EddyCorrect, EpiReg) + EddyCorrect, EpiReg, EddyQuad) from .dti import (BEDPOSTX, XFibres, DTIFit, ProbTrackX, ProbTrackX2, VecReg, ProjThresh, FindTheBiggest, DistanceMap, TractSkeleton, MakeDyadicVectors, BEDPOSTX5, XFibres5) diff --git a/nipype/interfaces/fsl/epi.py b/nipype/interfaces/fsl/epi.py index a13da0e0dc..cbd2772dd5 100644 --- a/nipype/interfaces/fsl/epi.py +++ b/nipype/interfaces/fsl/epi.py @@ -1232,3 +1232,217 @@ def _run_interface(self, runtime): if runtime.stderr: self.raise_exception(runtime) return runtime + + +class EddyQuadInputSpec(FSLCommandInputSpec): + base_name = traits.Str( + 'eddy_corrected', + argstr='%s', + desc="Basename (including path) specified when running EDDY", + position=0, + ) + idx_file = File( + exists=True, + mandatory=True, + argstr="--eddyIdx=%s", + desc=("File containing indices for all volumes into acquisition " + "parameters") + ) + param_file = File( + exists=True, + mandatory=True, + argstr="--eddyParams=%s", + desc="File containing acquisition parameters" + ) + mask_file = File( + exists=True, + mandatory=True, + argstr="--mask=%s", + desc="Binary mask file" + ) + bval_file = File( + exists=True, + mandatory=True, + argstr="--bvals=%s", + desc="b-values file" + ) + bvec_file = File( + exists=True, + mandatory=False, + argstr="--bvecs=%s", + desc=("b-vectors file - only used when .eddy_residuals " + "file is present") + ) + output_dir = traits.Str( + 'eddy_corrected.qc', + mandatory=False, + argstr='--output-dir=%s', + desc="Output directory - default = '.qc'", + ) + field = File( + mandatory=False, + argstr='--field=%s', + desc="TOPUP estimated field (in Hz)", + ) + slspec = File( + mandatory=False, + argstr='--slspec=%s', + desc="Text file specifying slice/group acquisition", + ) + verbose = traits.Bool( + False, + mandatory=False, + argstr='--verbose', + desc="Display debug messages", + ) + + +class EddyQuadOutputSpec(TraitedSpec): + out_qc_json = File( + exists=True, + mandatory=True, + desc=("Single subject database containing quality metrics and data " + "info.") + ) + out_qc_pdf = File( + exists=True, + mandatory=True, + desc="Single subject QC report." + ) + out_avg_b_png = traits.List( + File( + exists=True, + mandatory=True, + desc=("Image showing mid-sagittal, -coronal and -axial slices of " + "each averaged b-shell volume.") + ) + ) + out_avg_b0_png = traits.List( + File( + exists=True, + mandatory=False, + desc=("Image showing mid-sagittal, -coronal and -axial slices of " + "each averaged pe-direction b0 volume. Generated when using " + "the -f option.") + ) + ) + out_cnr_png = traits.List( + File( + exists=True, + mandatory=False, + desc=("Image showing mid-sagittal, -coronal and -axial slices of " + "each b-shell CNR volume. Generated when CNR maps are " + "available.") + ) + ) + out_vdm_png = File( + exists=True, + mandatory=False, + desc=("Image showing mid-sagittal, -coronal and -axial slices of " + "the voxel displacement map. Generated when using the -f " + "option.") + ) + out_residuals = File( + exists=True, + mandatory=False, + desc=("Text file containing the volume-wise mask-averaged squared " + "residuals. Generated when residual maps are available.") + ) + out_clean_volumes = File( + exists=True, + mandatory=False, + desc=("Text file containing a list of clean volumes, based on " + "the eddy squared residuals. To generate a version of the " + "pre-processed dataset without outlier volumes, use: " + "`fslselectvols -i -o " + "eddy_corrected_data_clean --vols=vols_no_outliers.txt`") + ) + + +class EddyQuad(FSLCommand): + """ + Interface for FSL eddy_quad, a tool for generating single subject reports + and storing the quality assessment indices for each subject. + `User guide `_ + + Examples + -------- + + >>> from nipype.interfaces.fsl import EddyQuad + >>> quad = EddyQuad() + >>> quad.inputs.in_file = 'epi.nii' + >>> quad.inputs.in_mask = 'epi_mask.nii' + >>> quad.inputs.in_index = 'epi_index.txt' + >>> quad.inputs.in_acqp = 'epi_acqp.txt' + >>> quad.inputs.in_bvec = 'bvecs.scheme' + >>> quad.inputs.in_bval = 'bvals.scheme' + >>> quad.inputs.use_cuda = True + >>> quad.cmdline # doctest: +ELLIPSIS + 'eddy_cuda --ff=10.0 --acqp=epi_acqp.txt --bvals=bvals.scheme \ +--bvecs=bvecs.scheme --imain=epi.nii --index=epi_index.txt \ +--mask=epi_mask.nii --niter=5 --nvoxhp=1000 --out=.../eddy_corrected' + >>> quad.cmdline # doctest: +ELLIPSIS + 'eddy_openmp --ff=10.0 --acqp=epi_acqp.txt --bvals=bvals.scheme \ +--bvecs=bvecs.scheme --imain=epi.nii --index=epi_index.txt \ +--mask=epi_mask.nii --niter=5 --nvoxhp=1000 --out=.../eddy_corrected' + >>> res = quad.run() # doctest: +SKIP + + """ + _cmd = 'eddy_quad' + input_spec = EddyQuadInputSpec + output_spec = EddyQuadOutputSpec + + def __init__(self, **inputs): + super(EddyQuad, self).__init__(**inputs) + + def _list_outputs(self): + outputs = self.output_spec().get() + out_dir = self.inputs.output_dir + outputs['out_qc_json'] = os.path.abspath( + os.path.join(self.inputs.output_dir, 'out.json') + ) + outputs['out_qc_json'] = os.path.abspath( + os.path.join(self.inputs.output_dir, 'out.json') + ) + outputs['out_parameter'] = os.path.abspath( + '%s.eddy_parameters' % self.inputs.out_base) + + # File generation might depend on the version of EDDY + out_rotated_bvecs = os.path.abspath( + '%s.eddy_rotated_bvecs' % self.inputs.out_base) + out_movement_rms = os.path.abspath( + '%s.eddy_movement_rms' % self.inputs.out_base) + out_restricted_movement_rms = os.path.abspath( + '%s.eddy_restricted_movement_rms' % self.inputs.out_base) + out_shell_alignment_parameters = os.path.abspath( + '%s.eddy_post_eddy_shell_alignment_parameters' % + self.inputs.out_base) + out_outlier_report = os.path.abspath( + '%s.eddy_outlier_report' % self.inputs.out_base) + if isdefined(self.inputs.cnr_maps) and self.inputs.cnr_maps: + out_cnr_maps = os.path.abspath( + '%s.eddy_cnr_maps.nii.gz' % self.inputs.out_base) + if os.path.exists(out_cnr_maps): + outputs['out_cnr_maps'] = out_cnr_maps + if isdefined(self.inputs.residuals) and self.inputs.residuals: + out_residuals = os.path.abspath( + '%s.eddy_residuals.nii.gz' % self.inputs.out_base) + if os.path.exists(out_residuals): + outputs['out_residuals'] = out_residuals + + if os.path.exists(out_rotated_bvecs): + outputs['out_rotated_bvecs'] = out_rotated_bvecs + if os.path.exists(out_movement_rms): + outputs['out_movement_rms'] = out_movement_rms + if os.path.exists(out_restricted_movement_rms): + outputs['out_restricted_movement_rms'] = \ + out_restricted_movement_rms + if os.path.exists(out_shell_alignment_parameters): + outputs['out_shell_alignment_parameters'] = \ + out_shell_alignment_parameters + if os.path.exists(out_outlier_report): + outputs['out_outlier_report'] = out_outlier_report + + return outputs + + From 9e4d5e897874675823066bd317b20c3988482ff1 Mon Sep 17 00:00:00 2001 From: akeshavan Date: Wed, 12 Dec 2018 10:13:10 -0800 Subject: [PATCH 02/13] fix: completed outputspec for EddyQuad --- nipype/interfaces/fsl/epi.py | 81 +++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/nipype/interfaces/fsl/epi.py b/nipype/interfaces/fsl/epi.py index cbd2772dd5..427553ab82 100644 --- a/nipype/interfaces/fsl/epi.py +++ b/nipype/interfaces/fsl/epi.py @@ -1304,11 +1304,13 @@ class EddyQuadOutputSpec(TraitedSpec): desc=("Single subject database containing quality metrics and data " "info.") ) + out_qc_pdf = File( exists=True, mandatory=True, desc="Single subject QC report." ) + out_avg_b_png = traits.List( File( exists=True, @@ -1317,6 +1319,7 @@ class EddyQuadOutputSpec(TraitedSpec): "each averaged b-shell volume.") ) ) + out_avg_b0_png = traits.List( File( exists=True, @@ -1326,6 +1329,7 @@ class EddyQuadOutputSpec(TraitedSpec): "the -f option.") ) ) + out_cnr_png = traits.List( File( exists=True, @@ -1335,6 +1339,7 @@ class EddyQuadOutputSpec(TraitedSpec): "available.") ) ) + out_vdm_png = File( exists=True, mandatory=False, @@ -1342,12 +1347,14 @@ class EddyQuadOutputSpec(TraitedSpec): "the voxel displacement map. Generated when using the -f " "option.") ) + out_residuals = File( exists=True, mandatory=False, desc=("Text file containing the volume-wise mask-averaged squared " "residuals. Generated when residual maps are available.") ) + out_clean_volumes = File( exists=True, mandatory=False, @@ -1396,52 +1403,48 @@ def __init__(self, **inputs): super(EddyQuad, self).__init__(**inputs) def _list_outputs(self): + from glob import glob outputs = self.output_spec().get() out_dir = self.inputs.output_dir outputs['out_qc_json'] = os.path.abspath( - os.path.join(self.inputs.output_dir, 'out.json') + os.path.join(out_dir, 'qc.json') ) - outputs['out_qc_json'] = os.path.abspath( - os.path.join(self.inputs.output_dir, 'out.json') + outputs['out_qc_pdf'] = os.path.abspath( + os.path.join(out_dir, 'qc.pdf') ) - outputs['out_parameter'] = os.path.abspath( - '%s.eddy_parameters' % self.inputs.out_base) - # File generation might depend on the version of EDDY - out_rotated_bvecs = os.path.abspath( - '%s.eddy_rotated_bvecs' % self.inputs.out_base) - out_movement_rms = os.path.abspath( - '%s.eddy_movement_rms' % self.inputs.out_base) - out_restricted_movement_rms = os.path.abspath( - '%s.eddy_restricted_movement_rms' % self.inputs.out_base) - out_shell_alignment_parameters = os.path.abspath( - '%s.eddy_post_eddy_shell_alignment_parameters' % - self.inputs.out_base) - out_outlier_report = os.path.abspath( - '%s.eddy_outlier_report' % self.inputs.out_base) - if isdefined(self.inputs.cnr_maps) and self.inputs.cnr_maps: - out_cnr_maps = os.path.abspath( - '%s.eddy_cnr_maps.nii.gz' % self.inputs.out_base) - if os.path.exists(out_cnr_maps): - outputs['out_cnr_maps'] = out_cnr_maps - if isdefined(self.inputs.residuals) and self.inputs.residuals: - out_residuals = os.path.abspath( - '%s.eddy_residuals.nii.gz' % self.inputs.out_base) - if os.path.exists(out_residuals): - outputs['out_residuals'] = out_residuals + outputs['out_avg_b0_png'] = glob(os.path.abspath( + os.path.join(out_dir, 'avg_b0_pe*.png') + )) - if os.path.exists(out_rotated_bvecs): - outputs['out_rotated_bvecs'] = out_rotated_bvecs - if os.path.exists(out_movement_rms): - outputs['out_movement_rms'] = out_movement_rms - if os.path.exists(out_restricted_movement_rms): - outputs['out_restricted_movement_rms'] = \ - out_restricted_movement_rms - if os.path.exists(out_shell_alignment_parameters): - outputs['out_shell_alignment_parameters'] = \ - out_shell_alignment_parameters - if os.path.exists(out_outlier_report): - outputs['out_outlier_report'] = out_outlier_report + outputs['out_avg_b_png'] = [b for b in glob(os.path.abspath( + os.path.join(out_dir, 'avg_b*.png') + )) if b not in outputs['out_avg_b0_png']] + + outputs['out_cnr_png'] = glob(os.path.abspath( + os.path.join(out_dir, 'cnr*.png') + )) + + vdm = os.path.abspath( + os.path.join(out_dir, 'vdm.png') + ) + + if os.path.exists(vdm): + outputs['out_vdm_png'] = vdm + + residuals = os.path.abspath( + os.path.join(out_dir, 'eddy_msr.txt') + ) + + if os.path.exists(residuals): + outputs['out_residuals'] = residuals + + outlier_vols = os.path.abspath( + os.path.join(out_dir, 'vols_no_outliers.txt') + ) + + if os.path.exists(outlier_vols): + outputs['out_clean_volumes'] = outlier_vols return outputs From 292d5fbd4d38e4b63e93506745af6c037666a74f Mon Sep 17 00:00:00 2001 From: akeshavan Date: Wed, 12 Dec 2018 10:53:14 -0800 Subject: [PATCH 03/13] fix: made the outputdir be mandatory and use the default val --- nipype/interfaces/fsl/epi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nipype/interfaces/fsl/epi.py b/nipype/interfaces/fsl/epi.py index 427553ab82..1aea937a51 100644 --- a/nipype/interfaces/fsl/epi.py +++ b/nipype/interfaces/fsl/epi.py @@ -1275,7 +1275,8 @@ class EddyQuadInputSpec(FSLCommandInputSpec): ) output_dir = traits.Str( 'eddy_corrected.qc', - mandatory=False, + mandatory=True, + usedefault=True, argstr='--output-dir=%s', desc="Output directory - default = '.qc'", ) From 8bfc9fabd2135f344179ec558dbe4f26b852748f Mon Sep 17 00:00:00 2001 From: Adam Richie-Halford Date: Wed, 12 Dec 2018 14:06:16 -0800 Subject: [PATCH 04/13] Add doctest for EddyQuad --- nipype/interfaces/fsl/epi.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/nipype/interfaces/fsl/epi.py b/nipype/interfaces/fsl/epi.py index 1aea937a51..b9e191744f 100644 --- a/nipype/interfaces/fsl/epi.py +++ b/nipype/interfaces/fsl/epi.py @@ -1378,21 +1378,19 @@ class EddyQuad(FSLCommand): >>> from nipype.interfaces.fsl import EddyQuad >>> quad = EddyQuad() - >>> quad.inputs.in_file = 'epi.nii' - >>> quad.inputs.in_mask = 'epi_mask.nii' - >>> quad.inputs.in_index = 'epi_index.txt' - >>> quad.inputs.in_acqp = 'epi_acqp.txt' - >>> quad.inputs.in_bvec = 'bvecs.scheme' - >>> quad.inputs.in_bval = 'bvals.scheme' - >>> quad.inputs.use_cuda = True + >>> quad.inputs.base_name = 'eddy_corrected' + >>> quad.inputs.idx_file = 'index.txt' + >>> quad.inputs.param_file = 'encfile.txt' + >>> quad.inputs.mask_file = 'mask.nii.gz' + >>> quad.inputs.bval_file = 'dwi.bval' + >>> quad.inputs.bvec_file = 'dwi.bvec' + >>> quad.inputs.output_dir = 'eddy_corrected.qc' + >>> quad.inputs.field = 'field.nii.gz' + >>> quad.verbose = True >>> quad.cmdline # doctest: +ELLIPSIS - 'eddy_cuda --ff=10.0 --acqp=epi_acqp.txt --bvals=bvals.scheme \ ---bvecs=bvecs.scheme --imain=epi.nii --index=epi_index.txt \ ---mask=epi_mask.nii --niter=5 --nvoxhp=1000 --out=.../eddy_corrected' - >>> quad.cmdline # doctest: +ELLIPSIS - 'eddy_openmp --ff=10.0 --acqp=epi_acqp.txt --bvals=bvals.scheme \ ---bvecs=bvecs.scheme --imain=epi.nii --index=epi_index.txt \ ---mask=epi_mask.nii --niter=5 --nvoxhp=1000 --out=.../eddy_corrected' + 'eddy_quad eddy_corrected --eddyIdx=index.txt --eddyParams=encfile.txt \ +--mask=mask.nii.gz --bvals=dwi.bval --bvecs=dwi.bvec \ +--output-dir=eddy_corrected.qc --field=field.nii.gz --verbose' >>> res = quad.run() # doctest: +SKIP """ From 48482fea7c04f39b72be546bf25f469c33b46a6c Mon Sep 17 00:00:00 2001 From: Adam Richie-Halford Date: Wed, 12 Dec 2018 20:49:37 -0800 Subject: [PATCH 05/13] Add name to .zenodo.json --- .zenodo.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.zenodo.json b/.zenodo.json index 9fccdcc316..01ea25ecd5 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -589,7 +589,12 @@ "affiliation": "MIT, HMS", "name": "Ghosh, Satrajit", "orcid": "0000-0002-5312-6729" - } + }, + { + "affiliation": "University of Washington", + "name": "Richie-Halford, Adam", + "orcid": "0000-0001-9276-9084" + }, ], "keywords": [ "neuroimaging", From 4c197753d988b0a73c267470f6a5c299afe30a61 Mon Sep 17 00:00:00 2001 From: Adam Richie-Halford Date: Thu, 13 Dec 2018 10:44:26 -0800 Subject: [PATCH 06/13] Fix tests for EddyQuad in interfaces/fsl/epi.py --- nipype/interfaces/fsl/epi.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/nipype/interfaces/fsl/epi.py b/nipype/interfaces/fsl/epi.py index b9e191744f..c4eeef6708 100644 --- a/nipype/interfaces/fsl/epi.py +++ b/nipype/interfaces/fsl/epi.py @@ -1379,18 +1379,19 @@ class EddyQuad(FSLCommand): >>> from nipype.interfaces.fsl import EddyQuad >>> quad = EddyQuad() >>> quad.inputs.base_name = 'eddy_corrected' - >>> quad.inputs.idx_file = 'index.txt' - >>> quad.inputs.param_file = 'encfile.txt' - >>> quad.inputs.mask_file = 'mask.nii.gz' - >>> quad.inputs.bval_file = 'dwi.bval' - >>> quad.inputs.bvec_file = 'dwi.bvec' + >>> quad.inputs.idx_file = 'epi_index.txt' + >>> quad.inputs.param_file = 'epi_acqp.txt' + >>> quad.inputs.mask_file = 'epi_mask.nii' + >>> quad.inputs.bval_file = 'bvals.scheme' + >>> quad.inputs.bvec_file = 'bvecs.scheme' >>> quad.inputs.output_dir = 'eddy_corrected.qc' - >>> quad.inputs.field = 'field.nii.gz' - >>> quad.verbose = True + >>> quad.inputs.field = 'fieldmap_phase_fslprepared.nii' + >>> quad.inputs.verbose = True >>> quad.cmdline # doctest: +ELLIPSIS - 'eddy_quad eddy_corrected --eddyIdx=index.txt --eddyParams=encfile.txt \ ---mask=mask.nii.gz --bvals=dwi.bval --bvecs=dwi.bvec \ ---output-dir=eddy_corrected.qc --field=field.nii.gz --verbose' + 'eddy_quad eddy_corrected --bvals=bvals.scheme --bvecs=bvecs.scheme \ +--field=fieldmap_phase_fslprepared.nii --eddyIdx=epi_index.txt \ +--mask=epi_mask.nii --output-dir=eddy_corrected.qc --eddyParams=epi_acqp.txt \ +--verbose' >>> res = quad.run() # doctest: +SKIP """ From 5675276d15db26d5aa1ec76e5ba9fb94640e725c Mon Sep 17 00:00:00 2001 From: Adam Richie-Halford Date: Fri, 14 Dec 2018 14:22:04 -0800 Subject: [PATCH 07/13] Edit in response to @effigies comments on PR #2825 - Remove redundant `__init__` method in `EddyQuad` class. - Use `os.path.abspath()` earlier in order to remove from later statements. - Improve `EddyQuad`'s `base_name` input description. - Remove unnecessary `mandatory=False` params. - Rename `slspec` to `slice_spec`. - Use default for `EddyQuad`'s `base_name` input. - Use a name template for `EddyQuad`'s `output_dir` input. --- nipype/interfaces/fsl/epi.py | 80 +++++++++++++++--------------------- 1 file changed, 33 insertions(+), 47 deletions(-) diff --git a/nipype/interfaces/fsl/epi.py b/nipype/interfaces/fsl/epi.py index c4eeef6708..1211fbc759 100644 --- a/nipype/interfaces/fsl/epi.py +++ b/nipype/interfaces/fsl/epi.py @@ -1237,8 +1237,10 @@ def _run_interface(self, runtime): class EddyQuadInputSpec(FSLCommandInputSpec): base_name = traits.Str( 'eddy_corrected', + usedefault=True, argstr='%s', - desc="Basename (including path) specified when running EDDY", + desc=("Basename (including path) for EDDY output files, i.e., " + "corrected images and QC files"), position=0, ) idx_file = File( @@ -1268,31 +1270,26 @@ class EddyQuadInputSpec(FSLCommandInputSpec): ) bvec_file = File( exists=True, - mandatory=False, argstr="--bvecs=%s", desc=("b-vectors file - only used when .eddy_residuals " "file is present") ) output_dir = traits.Str( - 'eddy_corrected.qc', - mandatory=True, - usedefault=True, + name_template='%s.qc', + name_source=['base_name'], argstr='--output-dir=%s', desc="Output directory - default = '.qc'", ) field = File( - mandatory=False, argstr='--field=%s', desc="TOPUP estimated field (in Hz)", ) - slspec = File( - mandatory=False, + slice_spec = File( argstr='--slspec=%s', desc="Text file specifying slice/group acquisition", ) verbose = traits.Bool( False, - mandatory=False, argstr='--verbose', desc="Display debug messages", ) @@ -1321,7 +1318,7 @@ class EddyQuadOutputSpec(TraitedSpec): ) ) - out_avg_b0_png = traits.List( + out_avg_b0_pe_png = traits.List( File( exists=True, mandatory=False, @@ -1399,52 +1396,41 @@ class EddyQuad(FSLCommand): input_spec = EddyQuadInputSpec output_spec = EddyQuadOutputSpec - def __init__(self, **inputs): - super(EddyQuad, self).__init__(**inputs) - def _list_outputs(self): - from glob import glob + import json outputs = self.output_spec().get() - out_dir = self.inputs.output_dir - outputs['out_qc_json'] = os.path.abspath( - os.path.join(out_dir, 'qc.json') - ) - outputs['out_qc_pdf'] = os.path.abspath( - os.path.join(out_dir, 'qc.pdf') - ) - - outputs['out_avg_b0_png'] = glob(os.path.abspath( - os.path.join(out_dir, 'avg_b0_pe*.png') - )) - - outputs['out_avg_b_png'] = [b for b in glob(os.path.abspath( - os.path.join(out_dir, 'avg_b*.png') - )) if b not in outputs['out_avg_b0_png']] + out_dir = os.path.abspath(self.inputs.output_dir) + outputs['out_qc_json'] = os.path.join(out_dir, 'qc.json') + outputs['out_qc_pdf'] = os.path.join(out_dir, 'qc.pdf') - outputs['out_cnr_png'] = glob(os.path.abspath( - os.path.join(out_dir, 'cnr*.png') - )) + with open(outputs['out_qc_json']) as fp: + qc = json.load(fp) - vdm = os.path.abspath( - os.path.join(out_dir, 'vdm.png') - ) + outputs['out_avg_b_png'] = [ + os.path.join(out_dir, 'avg_b{bval:d}.png'.format(bval=bval)) + for bval in list(set([0] + qc.get('data_unique_bvals'))) + ] - if os.path.exists(vdm): - outputs['out_vdm_png'] = vdm + if qc.get('qc_field_flag'): + outputs['out_avg_b0_pe_png'] = [ + os.path.join(out_dir, 'avg_b0_pe{i:d}'.format(i=i)) + for i in range(qc.get('data_no_PE_dirs')) + ] - residuals = os.path.abspath( - os.path.join(out_dir, 'eddy_msr.txt') - ) + outputs['out_vdm_png'] = os.path.join(out_dir, 'vdm.png') - if os.path.exists(residuals): - outputs['out_residuals'] = residuals + if qc.get('qc_cnr_flag'): + outputs['out_cnr_png'] = [ + os.path.join(out_dir, 'cnr{i:04d}.nii.gz.png') + for i, _ in enumerate(qc.get('qc_cnr_avg')) + ] - outlier_vols = os.path.abspath( - os.path.join(out_dir, 'vols_no_outliers.txt') - ) + if qc.get('qc_rss_flag'): + outputs['out_residuals'] = os.path.join(out_dir, 'eddy_msr.txt') - if os.path.exists(outlier_vols): - outputs['out_clean_volumes'] = outlier_vols + if qc.get('qc_ol_flag'): + outputs['out_clean_volumes'] = os.path.join(out_dir, + 'vols_no_outliers.txt') return outputs From 4a41f5ecedd4ad33722f4eaf84a2d4529efda877 Mon Sep 17 00:00:00 2001 From: Adam Richie-Halford Date: Fri, 18 Jan 2019 13:56:58 -0800 Subject: [PATCH 08/13] Add glob stuff back in --- .zenodo.json | 2 +- nipype/interfaces/fsl/epi.py | 81 ++++++++++++++++-------------------- 2 files changed, 37 insertions(+), 46 deletions(-) diff --git a/.zenodo.json b/.zenodo.json index 01ea25ecd5..c70775cf39 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -594,7 +594,7 @@ "affiliation": "University of Washington", "name": "Richie-Halford, Adam", "orcid": "0000-0001-9276-9084" - }, + } ], "keywords": [ "neuroimaging", diff --git a/nipype/interfaces/fsl/epi.py b/nipype/interfaces/fsl/epi.py index 1211fbc759..c69cbc87ce 100644 --- a/nipype/interfaces/fsl/epi.py +++ b/nipype/interfaces/fsl/epi.py @@ -1298,49 +1298,37 @@ class EddyQuadInputSpec(FSLCommandInputSpec): class EddyQuadOutputSpec(TraitedSpec): out_qc_json = File( exists=True, - mandatory=True, desc=("Single subject database containing quality metrics and data " "info.") ) out_qc_pdf = File( exists=True, - mandatory=True, desc="Single subject QC report." ) out_avg_b_png = traits.List( - File( - exists=True, - mandatory=True, - desc=("Image showing mid-sagittal, -coronal and -axial slices of " - "each averaged b-shell volume.") - ) + File(exists=True), + desc=("Image showing mid-sagittal, -coronal and -axial slices of " + "each averaged b-shell volume.") ) out_avg_b0_pe_png = traits.List( - File( - exists=True, - mandatory=False, - desc=("Image showing mid-sagittal, -coronal and -axial slices of " - "each averaged pe-direction b0 volume. Generated when using " - "the -f option.") - ) + File(exists=True), + desc=("Image showing mid-sagittal, -coronal and -axial slices of " + "each averaged pe-direction b0 volume. Generated when using " + "the -f option.") ) out_cnr_png = traits.List( - File( - exists=True, - mandatory=False, - desc=("Image showing mid-sagittal, -coronal and -axial slices of " - "each b-shell CNR volume. Generated when CNR maps are " - "available.") - ) + File(exists=True), + desc=("Image showing mid-sagittal, -coronal and -axial slices of " + "each b-shell CNR volume. Generated when CNR maps are " + "available.") ) out_vdm_png = File( exists=True, - mandatory=False, desc=("Image showing mid-sagittal, -coronal and -axial slices of " "the voxel displacement map. Generated when using the -f " "option.") @@ -1348,14 +1336,12 @@ class EddyQuadOutputSpec(TraitedSpec): out_residuals = File( exists=True, - mandatory=False, desc=("Text file containing the volume-wise mask-averaged squared " "residuals. Generated when residual maps are available.") ) out_clean_volumes = File( exists=True, - mandatory=False, desc=("Text file containing a list of clean volumes, based on " "the eddy squared residuals. To generate a version of the " "pre-processed dataset without outlier volumes, use: " @@ -1397,41 +1383,46 @@ class EddyQuad(FSLCommand): output_spec = EddyQuadOutputSpec def _list_outputs(self): - import json + from glob import glob outputs = self.output_spec().get() out_dir = os.path.abspath(self.inputs.output_dir) outputs['out_qc_json'] = os.path.join(out_dir, 'qc.json') outputs['out_qc_pdf'] = os.path.join(out_dir, 'qc.pdf') - with open(outputs['out_qc_json']) as fp: - qc = json.load(fp) - outputs['out_avg_b_png'] = [ os.path.join(out_dir, 'avg_b{bval:d}.png'.format(bval=bval)) for bval in list(set([0] + qc.get('data_unique_bvals'))) ] - if qc.get('qc_field_flag'): - outputs['out_avg_b0_pe_png'] = [ - os.path.join(out_dir, 'avg_b0_pe{i:d}'.format(i=i)) - for i in range(qc.get('data_no_PE_dirs')) - ] + # Grab all b* files here. This will also grab the b0_pe* files + # as well, but only if the field input was provided. So we'll remove + # them later in the next conditional. + outputs['out_avg_b_png'] = sorted(glob( + os.path.join(out_dir, 'avg_b*.png') + )) + + if isdefined(self.inputs.field): + outputs['out_avg_b0_pe_png'] = sorted(glob( + os.path.join(out_dir, 'avg_b0_pe*.png') + )) + + # The previous glob for `out_avg_b_png` also grabbed the + # `out_avg_b0_pe_png` files so we have to remove them + # from `out_avg_b_png`. + for fname in outputs['out_avg_b0_pe_png']: + outputs['out_avg_b_png'].remove(fname) outputs['out_vdm_png'] = os.path.join(out_dir, 'vdm.png') - if qc.get('qc_cnr_flag'): - outputs['out_cnr_png'] = [ - os.path.join(out_dir, 'cnr{i:04d}.nii.gz.png') - for i, _ in enumerate(qc.get('qc_cnr_avg')) - ] + outputs['out_cnr_png'] = sorted(glob(os.path.join(out_dir, 'cnr*.png'))) - if qc.get('qc_rss_flag'): - outputs['out_residuals'] = os.path.join(out_dir, 'eddy_msr.txt') + residuals = os.path.join(out_dir, 'eddy_msr.txt') + if os.path.isfile(residuals): + outputs['out_residuals'] = residuals - if qc.get('qc_ol_flag'): - outputs['out_clean_volumes'] = os.path.join(out_dir, - 'vols_no_outliers.txt') + clean_volumes = os.path.join(out_dir, 'vols_no_outliers.txt') + if os.path.isfile(clean_volumes): + outputs['out_clean_volumes'] = clean_volumes return outputs - From d01bf40d53ef158e040984c259c75464175af703 Mon Sep 17 00:00:00 2001 From: Adam Richie-Halford Date: Fri, 18 Jan 2019 14:03:48 -0800 Subject: [PATCH 09/13] Remove redundant out_avg_b_png lines --- nipype/interfaces/fsl/epi.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/nipype/interfaces/fsl/epi.py b/nipype/interfaces/fsl/epi.py index c69cbc87ce..ff3bda2479 100644 --- a/nipype/interfaces/fsl/epi.py +++ b/nipype/interfaces/fsl/epi.py @@ -1389,11 +1389,6 @@ def _list_outputs(self): outputs['out_qc_json'] = os.path.join(out_dir, 'qc.json') outputs['out_qc_pdf'] = os.path.join(out_dir, 'qc.pdf') - outputs['out_avg_b_png'] = [ - os.path.join(out_dir, 'avg_b{bval:d}.png'.format(bval=bval)) - for bval in list(set([0] + qc.get('data_unique_bvals'))) - ] - # Grab all b* files here. This will also grab the b0_pe* files # as well, but only if the field input was provided. So we'll remove # them later in the next conditional. From f6b49f5898b91068c46527887d20c0c9494b7261 Mon Sep 17 00:00:00 2001 From: Adam Richie-Halford Date: Fri, 18 Jan 2019 14:49:56 -0800 Subject: [PATCH 10/13] Add output_dir check to EddyQuad._list_outputs() --- nipype/interfaces/fsl/epi.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/nipype/interfaces/fsl/epi.py b/nipype/interfaces/fsl/epi.py index ff3bda2479..651af15cf6 100644 --- a/nipype/interfaces/fsl/epi.py +++ b/nipype/interfaces/fsl/epi.py @@ -1385,7 +1385,14 @@ class EddyQuad(FSLCommand): def _list_outputs(self): from glob import glob outputs = self.output_spec().get() - out_dir = os.path.abspath(self.inputs.output_dir) + + # If the output directory isn't defined, the interface seems to use + # the default but not set its value in `self.inputs.output_dir` + if not isdefined(self.inputs.output_dir): + out_dir = os.path.abspath(self.inputs.base_name + '.qc.nii.gz') + else: + out_dir = os.path.abspath(self.inputs.output_dir) + outputs['out_qc_json'] = os.path.join(out_dir, 'qc.json') outputs['out_qc_pdf'] = os.path.join(out_dir, 'qc.pdf') From 14027c59cb5f4e21e17bacafa1459271a16c1cbe Mon Sep 17 00:00:00 2001 From: Adam Richie-Halford Date: Fri, 18 Jan 2019 14:57:06 -0800 Subject: [PATCH 11/13] Use os.path.basename for the fallback output_dir in EddyQuad._list_outputs() --- nipype/interfaces/fsl/epi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nipype/interfaces/fsl/epi.py b/nipype/interfaces/fsl/epi.py index 651af15cf6..67a5ca86a2 100644 --- a/nipype/interfaces/fsl/epi.py +++ b/nipype/interfaces/fsl/epi.py @@ -1389,7 +1389,7 @@ def _list_outputs(self): # If the output directory isn't defined, the interface seems to use # the default but not set its value in `self.inputs.output_dir` if not isdefined(self.inputs.output_dir): - out_dir = os.path.abspath(self.inputs.base_name + '.qc.nii.gz') + out_dir = os.path.abspath(os.path.basename(self.inputs.base_name) + '.qc.nii.gz') else: out_dir = os.path.abspath(self.inputs.output_dir) From 51b7f45533fdf890ab6efeb6b1e05e6963a92ea1 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Sun, 20 Jan 2019 21:50:47 -0800 Subject: [PATCH 12/13] Apply minor edits from code review Co-Authored-By: richford --- nipype/interfaces/fsl/epi.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/nipype/interfaces/fsl/epi.py b/nipype/interfaces/fsl/epi.py index 67a5ca86a2..127a5519f9 100644 --- a/nipype/interfaces/fsl/epi.py +++ b/nipype/interfaces/fsl/epi.py @@ -1281,15 +1281,16 @@ class EddyQuadInputSpec(FSLCommandInputSpec): desc="Output directory - default = '.qc'", ) field = File( + exists=True, argstr='--field=%s', desc="TOPUP estimated field (in Hz)", ) slice_spec = File( + exists=True, argstr='--slspec=%s', desc="Text file specifying slice/group acquisition", ) verbose = traits.Bool( - False, argstr='--verbose', desc="Display debug messages", ) @@ -1301,45 +1302,38 @@ class EddyQuadOutputSpec(TraitedSpec): desc=("Single subject database containing quality metrics and data " "info.") ) - out_qc_pdf = File( exists=True, desc="Single subject QC report." ) - out_avg_b_png = traits.List( File(exists=True), desc=("Image showing mid-sagittal, -coronal and -axial slices of " "each averaged b-shell volume.") ) - out_avg_b0_pe_png = traits.List( File(exists=True), desc=("Image showing mid-sagittal, -coronal and -axial slices of " "each averaged pe-direction b0 volume. Generated when using " "the -f option.") ) - out_cnr_png = traits.List( File(exists=True), desc=("Image showing mid-sagittal, -coronal and -axial slices of " "each b-shell CNR volume. Generated when CNR maps are " "available.") ) - out_vdm_png = File( exists=True, desc=("Image showing mid-sagittal, -coronal and -axial slices of " "the voxel displacement map. Generated when using the -f " "option.") ) - out_residuals = File( exists=True, desc=("Text file containing the volume-wise mask-averaged squared " "residuals. Generated when residual maps are available.") ) - out_clean_volumes = File( exists=True, desc=("Text file containing a list of clean volumes, based on " @@ -1370,7 +1364,7 @@ class EddyQuad(FSLCommand): >>> quad.inputs.output_dir = 'eddy_corrected.qc' >>> quad.inputs.field = 'fieldmap_phase_fslprepared.nii' >>> quad.inputs.verbose = True - >>> quad.cmdline # doctest: +ELLIPSIS + >>> quad.cmdline 'eddy_quad eddy_corrected --bvals=bvals.scheme --bvecs=bvecs.scheme \ --field=fieldmap_phase_fslprepared.nii --eddyIdx=epi_index.txt \ --mask=epi_mask.nii --output-dir=eddy_corrected.qc --eddyParams=epi_acqp.txt \ From 41d82ad49065ec1059fbd32533762e665c741940 Mon Sep 17 00:00:00 2001 From: Adam Richie-Halford Date: Mon, 21 Jan 2019 06:58:38 -0800 Subject: [PATCH 13/13] Remove out_ prefix from EddyQuad outputs --- nipype/interfaces/fsl/epi.py | 42 ++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/nipype/interfaces/fsl/epi.py b/nipype/interfaces/fsl/epi.py index 127a5519f9..3e47576ec7 100644 --- a/nipype/interfaces/fsl/epi.py +++ b/nipype/interfaces/fsl/epi.py @@ -1297,44 +1297,44 @@ class EddyQuadInputSpec(FSLCommandInputSpec): class EddyQuadOutputSpec(TraitedSpec): - out_qc_json = File( + qc_json = File( exists=True, desc=("Single subject database containing quality metrics and data " "info.") ) - out_qc_pdf = File( + qc_pdf = File( exists=True, desc="Single subject QC report." ) - out_avg_b_png = traits.List( + avg_b_png = traits.List( File(exists=True), desc=("Image showing mid-sagittal, -coronal and -axial slices of " "each averaged b-shell volume.") ) - out_avg_b0_pe_png = traits.List( + avg_b0_pe_png = traits.List( File(exists=True), desc=("Image showing mid-sagittal, -coronal and -axial slices of " "each averaged pe-direction b0 volume. Generated when using " "the -f option.") ) - out_cnr_png = traits.List( + cnr_png = traits.List( File(exists=True), desc=("Image showing mid-sagittal, -coronal and -axial slices of " "each b-shell CNR volume. Generated when CNR maps are " "available.") ) - out_vdm_png = File( + vdm_png = File( exists=True, desc=("Image showing mid-sagittal, -coronal and -axial slices of " "the voxel displacement map. Generated when using the -f " "option.") ) - out_residuals = File( + residuals = File( exists=True, desc=("Text file containing the volume-wise mask-averaged squared " "residuals. Generated when residual maps are available.") ) - out_clean_volumes = File( + clean_volumes = File( exists=True, desc=("Text file containing a list of clean volumes, based on " "the eddy squared residuals. To generate a version of the " @@ -1387,38 +1387,38 @@ def _list_outputs(self): else: out_dir = os.path.abspath(self.inputs.output_dir) - outputs['out_qc_json'] = os.path.join(out_dir, 'qc.json') - outputs['out_qc_pdf'] = os.path.join(out_dir, 'qc.pdf') + outputs['qc_json'] = os.path.join(out_dir, 'qc.json') + outputs['qc_pdf'] = os.path.join(out_dir, 'qc.pdf') # Grab all b* files here. This will also grab the b0_pe* files # as well, but only if the field input was provided. So we'll remove # them later in the next conditional. - outputs['out_avg_b_png'] = sorted(glob( + outputs['avg_b_png'] = sorted(glob( os.path.join(out_dir, 'avg_b*.png') )) if isdefined(self.inputs.field): - outputs['out_avg_b0_pe_png'] = sorted(glob( + outputs['avg_b0_pe_png'] = sorted(glob( os.path.join(out_dir, 'avg_b0_pe*.png') )) - # The previous glob for `out_avg_b_png` also grabbed the - # `out_avg_b0_pe_png` files so we have to remove them - # from `out_avg_b_png`. - for fname in outputs['out_avg_b0_pe_png']: - outputs['out_avg_b_png'].remove(fname) + # The previous glob for `avg_b_png` also grabbed the + # `avg_b0_pe_png` files so we have to remove them + # from `avg_b_png`. + for fname in outputs['avg_b0_pe_png']: + outputs['avg_b_png'].remove(fname) - outputs['out_vdm_png'] = os.path.join(out_dir, 'vdm.png') + outputs['vdm_png'] = os.path.join(out_dir, 'vdm.png') - outputs['out_cnr_png'] = sorted(glob(os.path.join(out_dir, 'cnr*.png'))) + outputs['cnr_png'] = sorted(glob(os.path.join(out_dir, 'cnr*.png'))) residuals = os.path.join(out_dir, 'eddy_msr.txt') if os.path.isfile(residuals): - outputs['out_residuals'] = residuals + outputs['residuals'] = residuals clean_volumes = os.path.join(out_dir, 'vols_no_outliers.txt') if os.path.isfile(clean_volumes): - outputs['out_clean_volumes'] = clean_volumes + outputs['clean_volumes'] = clean_volumes return outputs