Nipype Showcase

What's all the hype about Nipype? Is it really that good? Short answer: Yes!

Long answer: ... well, let's consider a very simple fMRI preprocessing workflow that just performs:

  1. slice time correction
  2. motion correction
  3. smoothing

Preparing the preprocessing workflow

First, we need to import the main Nipype tools: Node and Workflow


In [ ]:
from nipype import Node, Workflow

Now, we can import the interfaces that we want to use for the preprocessing.


In [ ]:
from nipype.interfaces.fsl import SliceTimer, MCFLIRT, Smooth

Next, we will put the three interfaces into a node and define the specific input parameters.


In [ ]:
# Initiate a node to correct for slice wise acquisition
slicetimer = Node(SliceTimer(index_dir=False,
                             interleaved=True,
                             time_repetition=2.5),
                  name="slicetimer")

In [ ]:
# Initiate a node to correct for motion
mcflirt = Node(MCFLIRT(mean_vol=True,
                       save_plots=True),
               name="mcflirt")

In [ ]:
# Initiate a node to smooth functional images
smooth = Node(Smooth(fwhm=4), name="smooth")

After creating the nodes, we can now create the preprocessing workflow.


In [ ]:
preproc01 = Workflow(name='preproc01', base_dir='.')

Now, we can put all the nodes into this preprocessing workflow. We specify the data flow / execution flow of the workflow by connecting the corresponding nodes to each other.


In [ ]:
preproc01.connect([(slicetimer, mcflirt, [('slice_time_corrected_file', 'in_file')]),
                   (mcflirt, smooth, [('out_file', 'in_file')])])

To better understand what we did we can write out the workflow graph and visualize it directly in this notebook.


In [ ]:
preproc01.write_graph(graph2use='orig')

In [ ]:
# Visualize graph
from IPython.display import Image
Image(filename="preproc01/graph_detailed.png")

Run the workflow on one functional image

Now, that we've created a workflow, let's run it on a functional image.

For this, we first need to specify the input file of the very first node, i.e. the slicetimer node.


In [ ]:
slicetimer.inputs.in_file = '/data/ds000114/sub-01/ses-test/func/sub-01_ses-test_task-fingerfootlips_bold.nii.gz'

To show off Nipype's parallelization power, let's run the workflow in parallel, on 5 processors and let's show the execution time:


In [ ]:
%time preproc01.run('MultiProc', plugin_args={'n_procs': 5})

Conclusion

Nice, the whole execution took ~2min. But wait... The parallelization didn't really help.

That's true, but because there was no possibility to run the workflow in parallel. Each node depends on the output of the previous node.

Results of preproc01

So, what did we get? Let's look at the output folder preproc01:


In [ ]:
!tree preproc01 -I '*js|*json|*pklz|_report|*.dot|*html'

Rerunning of a workflow

Now, for fun. Let's run the workflow again, but let's change the fwhm value of the Gaussian smoothing kernel to 2.


In [ ]:
smooth.inputs.fwhm = 2

And let's run the workflow again.


In [ ]:
%time preproc01.run('MultiProc', plugin_args={'n_procs': 5})

Conclusion

Interesting, now it only took ~15s to execute the whole workflow again. What happened?

As you can see from the log above, Nipype didn't execute the two nodes slicetimer and mclfirt again. This, because their input values didn't change from the last execution. The preproc01 workflow therefore only had to rerun the node smooth.

Running a workflow in parallel

Ok, ok... Rerunning a workflow again is faster. That's nice and all, but I want more. You spoke of parallel execution!

We saw that the preproc01 workflow takes about ~2min to execute completely. So, if we would run the workflow on five functional images, it should take about ~10min total. This, of course, assuming the execution will be done sequentially. Now, let's see how long it takes if we run it in parallel.


In [ ]:
# First, let's copy/clone 'preproc01'
preproc02 = preproc01.clone('preproc02')
preproc03 = preproc01.clone('preproc03')
preproc04 = preproc01.clone('preproc04')
preproc05 = preproc01.clone('preproc05')

We now have five different preprocessing workflows. If we want to run them in parallel, we can put them all in another workflow.


In [ ]:
metaflow = Workflow(name='metaflow', base_dir='.')

In [ ]:
# Now we can add the five preproc workflows to the bigger metaflow
metaflow.add_nodes([preproc01, preproc02, preproc03,
                    preproc04, preproc05])

Note: We now have a workflow (metaflow), that contains five other workflows (preproc0?), each of them containing three nodes.

To better understand this, let's visualize this metaflow.


In [ ]:
# As before, let's write the graph of the workflow
metaflow.write_graph(graph2use='flat')

In [ ]:
# And visualize the graph
from IPython.display import Image
Image(filename="metaflow/graph_detailed.png")

Ah... so now we can see that the metaflow has potential for parallelization. So let's put it to test


In [ ]:
%time metaflow.run('MultiProc', plugin_args={'n_procs': 5})

This time we can see that Nipype uses all available processors.

And if all went well, the total execution time should still be around ~2min.

That's why Nipype is so amazing. The days of opening multiple SPMs, FSLs, AFNIs etc. are past!

Results of metaflow


In [ ]:
!tree metaflow -I '*js|*json|*pklz|_report|*.dot|*html'