from nipype import Node, Workflow

"""
Explanation: 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
End of explanation
"""


from nipype.interfaces.fsl import SliceTimer, MCFLIRT, Smooth

"""
Explanation: Now, we can import the interfaces that we want to use for the preprocessing.
End of explanation
"""


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

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

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

"""
Explanation: Next, we will put the three interfaces into a node and define the specific input parameters.
End of explanation
"""


preproc01 = Workflow(name='preproc01', base_dir='.')

"""
Explanation: After creating the nodes, we can now create the preprocessing workflow.
End of explanation
"""


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

"""
Explanation: 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.
End of explanation
"""


preproc01.write_graph(graph2use='orig')

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

"""
Explanation: To better understand what we did we can write out the workflow graph and visualize it directly in this notebook.
End of explanation
"""


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

"""
Explanation: 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.
End of explanation
"""


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

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


!tree preproc01 -I '*js|*json|*pklz|_report|*.dot|*html'

"""
Explanation: 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:
End of explanation
"""


smooth.inputs.fwhm = 2

"""
Explanation: 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.
End of explanation
"""


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

"""
Explanation: And let's run the workflow again.
End of explanation
"""


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

"""
Explanation: 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.
End of explanation
"""


metaflow = Workflow(name='metaflow', base_dir='.')

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

"""
Explanation: We now have five different preprocessing workflows. If we want to run them in parallel, we can put them all in another workflow.
End of explanation
"""


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

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

"""
Explanation: 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.
End of explanation
"""


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

"""
Explanation: Ah... so now we can see that the metaflow has potential for parallelization. So let's put it to test
End of explanation
"""


!tree metaflow -I '*js|*json|*pklz|_report|*.dot|*html'

"""
Explanation: 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
End of explanation
"""


