Satra once called the
Function module, the "do anything you want card". Which is a perfect description. Because it allows you to put any code you want into an empty node, which you then can put in your workflow exactly where it needs to be.
You might have already seen the
Function module in the example section in the Node tutorial. Let's take a closer look at it again.
The most important component of a working
Function interface is a Python function. There are several ways to associate a function with a
Function interface, but the most common way will involve functions you code yourself as part of your Nipype scripts. Consider the following function:
# Create a small example function def add_two(x_input): return x_input + 2
This simple function takes a value, adds 2 to it, and returns that new value.
Just as Nipype interfaces have inputs and outputs, Python functions have inputs, in the form of parameters or arguments, and outputs, in the form of their return values. When you define a Function interface object with an existing function, as in the case of
add_two() above, you must pass the constructor information about the function's inputs, its outputs, and the function itself. For example,
# Import Node and Function module from nipype import Node, Function # Create Node addtwo = Node(Function(input_names=["x_input"], output_names=["val_output"], function=add_two), name='add_node')
Then you can set the inputs and run just as you would with any other interface:
addtwo.inputs.x_input = 4 addtwo.run()
180514-09:15:13,890 workflow INFO: [Node] Setting-up "add_node" in "/tmp/tmpzai5mt_h/add_node". 180514-09:15:13,893 workflow INFO: [Node] Running "add_node" ("nipype.interfaces.utility.wrappers.Function") 180514-09:15:13,900 workflow INFO: [Node] Finished "add_node".
<nipype.interfaces.base.support.InterfaceResult at 0x7fe2f6fc5c18>
val_output = 6
You need to be careful that the name of the input paramter to the node is the same name as the input parameter to the function, i.e.
x_input. But you don't have to specify
output_names. You can also just use:
addtwo = Node(Function(function=add_two), name='add_node') addtwo.inputs.x_input = 8 addtwo.run()
180514-09:15:13,931 workflow INFO: [Node] Setting-up "add_node" in "/tmp/tmp1s50vdl0/add_node". 180514-09:15:13,935 workflow INFO: [Node] Running "add_node" ("nipype.interfaces.utility.wrappers.Function") 180514-09:15:13,939 workflow INFO: [Node] Finished "add_node".
<nipype.interfaces.base.support.InterfaceResult at 0x7fe2f6f65e48>
out = 10
Chances are, you will want to write functions that do more complicated processing, particularly using the growing stack of Python packages geared towards neuroimaging, such as Nibabel, Nipy, or PyMVPA.
While this is completely possible (and, indeed, an intended use of the Function interface), it does come with one important constraint. The function code you write is executed in a standalone environment, which means that any external functions or classes you use have to be imported within the function itself:
def get_n_trs(in_file): import nibabel f = nibabel.load(in_file) return f.shape[-1]
Without explicitly importing Nibabel in the body of the function, this would fail.
Alternatively, it is possible to provide a list of strings corresponding to the imports needed to execute a function as a parameter of the
Function constructor. This allows for the use of external functions that do not import all external definitions inside the function body.
To use an existing function object (as we have been doing so far) with a Function interface, it must be passed to the constructor. However, it is also possible to dynamically set how a Function interface will process its inputs using the special
This input takes not a function object, but actually a single string that can be parsed to define a function. In the equivalent case to our example above, the string would be
add_two_str = "def add_two(val):\n return val + 2\n"
Unlike when using a function object, this input can be set like any other, meaning that you could write a function that outputs different function strings depending on some run-time contingencies, and connect that output the
function_str input of a downstream Function interface.
There's only one trap that you should be aware of when using the
If you want to use another module inside a function, you have to import it again inside the function. Let's take a look at the following example:
from nipype import Node, Function # Create the Function object def get_random_array(array_shape): # Import random function from numpy.random import random return random(array_shape) # Create Function Node that executes get_random_array rndArray = Node(Function(input_names=["array_shape"], output_names=["random_array"], function=get_random_array), name='rndArray_node') # Specify the array_shape of the random array rndArray.inputs.array_shape = (3, 3) # Run node rndArray.run() # Print output print(rndArray.result.outputs)
180514-09:15:13,991 workflow INFO: [Node] Setting-up "rndArray_node" in "/tmp/tmp1m42azlt/rndArray_node". 180514-09:15:13,995 workflow INFO: [Node] Running "rndArray_node" ("nipype.interfaces.utility.wrappers.Function") 180514-09:15:14,1 workflow INFO: [Node] Finished "rndArray_node". random_array = [[0.76392687 0.22652322 0.71572652] [0.62546441 0.97167394 0.68227735] [0.51375615 0.76636153 0.84837518]]
Now, let's see what happens if we move the import of
random outside the scope of
from nipype import Node, Function # Import random function from numpy.random import random # Create the Function object def get_random_array(array_shape): return random(array_shape) # Create Function Node that executes get_random_array rndArray = Node(Function(input_names=["array_shape"], output_names=["random_array"], function=get_random_array), name='rndArray_node') # Specify the array_shape of the random array rndArray.inputs.array_shape = (3, 3) # Run node try: rndArray.run() except(NameError) as err: print("NameError:", err) else: raise
180514-09:15:14,32 workflow INFO: [Node] Setting-up "rndArray_node" in "/tmp/tmpfttbq7xq/rndArray_node". 180514-09:15:14,39 workflow INFO: [Node] Running "rndArray_node" ("nipype.interfaces.utility.wrappers.Function") 180514-09:15:14,45 workflow WARNING: [Node] Error on "rndArray_node" (/tmp/tmpfttbq7xq/rndArray_node) NameError: name 'random' is not defined
As you can see, if we don't import
random inside the scope of the function, we receive the following error:
NameError: global name 'random' is not defined Interface Function failed to run.