Function Interface

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.

A Simple Function Interface

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()
211017-17:15:32,821 nipype.workflow INFO:
	 [Node] Setting-up "add_node" in "/tmp/tmpvz5_ju1z/add_node".
211017-17:15:32,827 nipype.workflow INFO:
	 [Node] Running "add_node" ("nipype.interfaces.utility.wrappers.Function")
211017-17:15:32,841 nipype.workflow INFO:
	 [Node] Finished "add_node".
<nipype.interfaces.base.support.InterfaceResult at 0x7f66484368d0>
addtwo.result.outputs
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 input_names or output_names. You can also just use:

addtwo = Node(Function(function=add_two), name='add_node')
addtwo.inputs.x_input = 8
addtwo.run()
211017-17:15:32,876 nipype.workflow INFO:
	 [Node] Setting-up "add_node" in "/tmp/tmpai956m2q/add_node".
211017-17:15:32,882 nipype.workflow INFO:
	 [Node] Running "add_node" ("nipype.interfaces.utility.wrappers.Function")
211017-17:15:32,888 nipype.workflow INFO:
	 [Node] Finished "add_node".
<nipype.interfaces.base.support.InterfaceResult at 0x7f6648488450>
addtwo.result.outputs
out = 10

Using External Packages

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.

Advanced Use

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 function_str input.

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.

Important - Function Nodes are closed environments

There’s only one trap that you should be aware of when using the Function module.

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)
211017-17:15:32,932 nipype.workflow INFO:
	 [Node] Setting-up "rndArray_node" in "/tmp/tmpivstknpm/rndArray_node".
211017-17:15:32,937 nipype.workflow INFO:
	 [Node] Running "rndArray_node" ("nipype.interfaces.utility.wrappers.Function")
211017-17:15:32,952 nipype.workflow INFO:
	 [Node] Finished "rndArray_node".

random_array = [[0.10980786 0.24955201 0.54628587]
 [0.50111466 0.91632967 0.19999327]
 [0.6824793  0.28224087 0.31246336]]

Now, let’s see what happens if we move the import of random outside the scope of get_random_array:

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
211017-17:15:32,971 nipype.workflow INFO:
	 [Node] Setting-up "rndArray_node" in "/tmp/tmp8e4aw8dl/rndArray_node".
211017-17:15:32,976 nipype.workflow INFO:
	 [Node] Running "rndArray_node" ("nipype.interfaces.utility.wrappers.Function")
211017-17:15:32,984 nipype.workflow WARNING:
	 Storing result file without outputs
211017-17:15:32,986 nipype.workflow WARNING:
	 [Node] Error on "rndArray_node" (/tmp/tmp8e4aw8dl/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.