Skip to content

[ENH]: add list_fwhm option to create_susan_smooth #2233

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 13, 2017

Conversation

jdkent
Copy link
Contributor

@jdkent jdkent commented Oct 17, 2017

helps with nipreps/fmriprep#706

Changes proposed in this pull request

  • add list_fwhms option to create_susan_smooth to allow a list of fwhms to be run, instead of one.

@codecov-io
Copy link

codecov-io commented Oct 17, 2017

Codecov Report

Merging #2233 into master will decrease coverage by 0.01%.
The diff coverage is 27.27%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master   #2233      +/-   ##
=========================================
- Coverage   72.31%   72.3%   -0.02%     
=========================================
  Files        1182    1182              
  Lines       58947   58954       +7     
  Branches     8490    8494       +4     
=========================================
- Hits        42628   42627       -1     
- Misses      14935   14944       +9     
+ Partials     1384    1383       -1
Flag Coverage Δ
#smoketests 72.3% <27.27%> (-0.02%) ⬇️
#unittests 69.87% <27.27%> (-0.02%) ⬇️
Impacted Files Coverage Δ
nipype/workflows/fmri/fsl/preprocess.py 84.97% <27.27%> (-1.94%) ⬇️
nipype/pipeline/plugins/multiproc.py 76.76% <0%> (-1.41%) ⬇️
nipype/pipeline/engine/utils.py 79.55% <0%> (+0.13%) ⬆️
nipype/interfaces/utility/wrappers.py 86.41% <0%> (+1.23%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 29756d3...ed92f7e. Read the comment docs.

Copy link
Member

@effigies effigies left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi James. This looks good, logic-wise, but I think we can simplify it a bit.

mask_files = [mask_files]
multi_in_files = [in_file for in_file in in_files for fwhm in fwhms]
multi_mask_files = [mask_file for mask_file in mask_files for fwhm in fwhms]
multi_fwhms = [fwhm for fwhm in fwhms for in_file in in_files]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You always want to use the same order, or else you're not getting the Cartesian product. So this should be:

multi_fwhms = [fwhm for in_file in in files for fwhm in fwhms]

else:
smooth = pe.MapNode(interface=fsl.SUSAN(),
iterfield=['in_file', 'brightness_threshold', 'usans'],
name='smooth')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop the if statement, and just use the first part:

multi_inputs = pe.Node(
    util.Function(function=cartesian_product,
                  output_names=['in_file', 'fwhm', 'usans', 'btthresh']),
    name='multi_inputs')  # It's okay if we never hook this up

smooth = pe.MapNode(
    fsl.SUSAN(), iterfield=['in_file', 'brightness_threshold', 'usans'],
    name='smooth')

I'm also dropping the input_names from Function, as it is now redundant and a potential source of error. And I make the output match the expected input names to SUSAN. Finally, I give more descriptive names to your merge_out and median_out. You don't need to make all of these changes if you feel strongly about them, but at least consider them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, so if I exclude input_names, the Function interface infers the names of the inputs from the definition of the function? I agree with giving more descriptive names for merge_out and median_out. I also changed change the output_names to cart_<name>, to make it clear they are Cartesian lists (and not redefine usans and btthresh)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup. Variable names are accessible parts of function objects. (Not true of output names. That's always just a tuple.)

Those changes sound good.

@@ -767,11 +767,12 @@ def create_susan_smooth(name="susan_smooth", separate_masks=True):

name : name of workflow (default: susan_smooth)
separate_masks : separate masks for each run
list_fwhms : multiple full wide half maximum smoothing kernels
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would drop this entirely. You can just now accept multiple floats, and treat a single float as a list of length 1.

# replaces the functionality of a "for loop"
def cartesian_product(fwhms, in_files, mask_files, merge_out, median_out):
if type(in_files) == str:
in_files = [in_files]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's already code to do this work for you.

from nipype.utils.filemanip import filename_to_list
in_files = filename_to_list(in_files)

You can't use this for numbers, but to treat one FWHM as a list of length one:

fwhms = [fwhms] if isinstance(fwhms, (int, float)) else fwhms

if type(in_files) == str:
in_files = [in_files]
if type(mask_files) == str:
mask_files = [mask_files]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you don't actually use multi_mask_files, so I'd drop mask_files and multi_mask_files from this function entirely.

susan_smooth.connect(inputnode, 'in_files', smooth, 'in_file')
susan_smooth.connect(inputnode, 'fwhm', smooth, 'fwhm')
susan_smooth.connect(median, ('out_stat', getbtthresh), smooth, 'brightness_threshold')
susan_smooth.connect(merge, ('out', getusans), smooth, 'usans')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will be more readable with a multi-connect statement:

susan_smooth.connect([
    (inputnode, multi_inputs, [('in_files', 'in_files'),
                               ('fwhm', 'fwhms')]),
    (median, multi_inputs, [(('out_stat', getbtthresh), 'btthresh')]),
    (merge, multi_inputs, [(('out', getusans), 'usans')]),
    (multi_inputs, smooth, [('in_file', 'in_file'),
                            ('fwhm', 'fwhm'),
                            ('btthresh', 'brightness_threshold'),
                            ('usans', 'usans')]),
    ])

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so too, but I didn't want to change the way they had it in case that was their coding standard, but with your recommendation I'll change it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My feeling is this workflow was written to be readable, as an example for writing them. I think having a bit of a mix where we choose the most readable at any given point is within the spirit of the thing.


Inputs::

inputnode.in_files : functional runs (filename or list of filenames)
inputnode.fwhm : fwhm for smoothing with SUSAN
inputnode.fwhm : fwhm for smoothing with SUSAN (float or list of floats)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doc update is good, though. Let's keep that.

Copy link
Member

@effigies effigies left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@jdkent Are you testing downstream to make sure that it's doing what you want?

@jdkent
Copy link
Contributor Author

jdkent commented Oct 17, 2017

@effigies I have tested the workflow locally using a single in_file, mask_file with a list of fwhm, and using a list of in_file, mask_file, with a list of fwhm, they gave expected output. The most basic case is tested in the doc string, yes?

off topic: Do you have any resources for learning how to write tests (unit tests, regression tests) for python repositories like nipype and fmriprep?

@effigies
Copy link
Member

The doctest doesn't actually get run during testing (see +SKIP). It's just an example.

How about creating a niworkflows branch that pins against this PR, and then your FMRIPREP branch to that?

In case you haven't updated the nipype pin before:

git fetch upstream
git checkout -b pin/nipype_pr2233 upstream/master
sed -i -e 's/nipy\//jdkent\//' .gitmodules
git submodule sync
git submodule foreach git fetch origin
git submodule foreach git checkout origin/enh-create_susan_smooth
git add .gitmodules nipype
git commit -m 'PIN: jdkent/nipype feature branch'
git push -u origin pin/nipype_pr2233

I think you're familiar with pinning fmriprep against a niworkflows branch?

@effigies
Copy link
Member

In fmriprep, our version of regression tests are really just running the entire workflow on truncated datasets with every commit. Our unit tests are sorely lacking.

In nipype, if I'm adding a new feature, I try to look at the tests for nearby features (mostly using grep or searching in GitHub), and see what I can do to exercise the new behavior. Then I mostly depend on Satra to decide whether it's reasonable. And if you're fixing a bug, trying to create a test that fails before but passes after is a good strategy.

But I don't have any references that I regularly recommend. Perhaps I should try to evaluate them some time. Might be worth asking on the brainhack Slack to see if people have suggestions.

@effigies
Copy link
Member

Oh, sorry. Before any of that git submodule stuff, you may want to do:

git submodule init
git submodule update

@jdkent
Copy link
Contributor Author

jdkent commented Oct 18, 2017

oops, probably should have done that first, I eventually modified your instructions from the ICA-AROMA commit. I believe it followed something like the following. So the important bit was
git submodule init
git submodule update

cd niworkflows
git submodule init
git submodule update
git checkout -b pin/nipype_pr2233 upstream/master
sed -i -e 's/nipy\//jdkent\//' .gitmodules
git -C nipype fetch origin
git -C nipype checkout origin/enh-create_susan_smooth
git checkout -b update_pin
git add nipype .gitmodules
git commit -m 'PIN: jdkent/nipype feature branch'
git push -u origin pin/nipype_pr2233

@satra
Copy link
Member

satra commented Oct 21, 2017

@effigies - please merge this when ready. i got a little lost with the git submodule comments.

@effigies
Copy link
Member

Sounds good. (Submodule stuff is for downstream testing in an fmriprep branch.)

@mgxd mgxd added this to the 0.14.0 milestone Oct 24, 2017
@mgxd
Copy link
Member

mgxd commented Nov 7, 2017

is this all set?

@jdkent
Copy link
Contributor Author

jdkent commented Nov 8, 2017

I haven't found the time to test it rigorously downstream (in FMRIPREP), but this does work as expected in simpler examples.

@effigies
Copy link
Member

effigies commented Nov 8, 2017 via email

@effigies effigies merged commit f38f021 into nipy:master Nov 13, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants