Skip to content
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

Common pipeline functions in narps_open.core #128

Merged
merged 18 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ Here are the available topics :
* :microscope: [testing](/docs/testing.md) details the testing features of the project, i.e.: how is the code tested ?
* :package: [ci-cd](/docs/ci-cd.md) contains the information on how continuous integration and delivery (knowned as CI/CD) is set up.
* :writing_hand: [pipeline](/docs/pipelines.md) tells you all you need to know in order to write pipelines
* :compass: [core](/docs/core.md) a list of helpful functions when writing pipelines
* :vertical_traffic_light: [status](/docs/status.md) contains the information on how to get the work progress status for a pipeline.
117 changes: 117 additions & 0 deletions docs/core.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Core functions you can use to write pipelines

Here are a few functions that could be useful for creating a pipeline with Nipype. These functions are meant to stay as unitary as possible.

These are intended to be inserted in a nipype.Workflow inside a [nipype.Function](https://nipype.readthedocs.io/en/latest/api/generated/nipype.interfaces.utility.wrappers.html#function) interface, or for some of them (see associated docstring) as part of a [nipype.Workflow.connect](https://nipype.readthedocs.io/en/latest/api/generated/nipype.pipeline.engine.workflows.html#nipype.pipeline.engine.workflows.Workflow.connect) method.

In the following example, we use the `list_intersection` function of `narps_open.core.common`, in both of the mentioned cases.

```python
from nipype import Node, Function, Workflow
from narps_open.core.common import list_intersection

# First case : a Function Node
intersection_node = Node(Function(
function = list_intersection,
input_names = ['list_1', 'list_2'],
output_names = ['output']
), name = 'intersection_node')
intersection_node.inputs.list_1 = ['001', '002', '003', '004']
intersection_node.inputs.list_2 = ['002', '004', '005']
print(intersection_node.run().outputs.output) # ['002', '004']

# Second case : inside a connect node
# We assume that there is a node_0 returning ['001', '002', '003', '004'] as `output` value
test_workflow = Workflow(
base_dir = '/path/to/base/dir',
name = 'test_workflow'
)
test_workflow.connect([
# node_1 will receive the evaluation of :
# list_intersection(['001', '002', '003', '004'], ['002', '004', '005'])
# as in_value
(node_0, node_1, [(('output', list_intersection, ['002', '004', '005']), 'in_value')])
])
test_workflow.run()
```

> [!TIP]
> Use a [nipype.MapNode](https://nipype.readthedocs.io/en/latest/api/generated/nipype.pipeline.engine.nodes.html#nipype.pipeline.engine.nodes.MapNode) to run these functions on lists instead of unitary contents. E.g.: the `remove_file` function of `narps_open.core.common` only removes one file at a time, but feel free to pass a list of files using a `nipype.MapNode`.

```python
from nipype import MapNode, Function
from narps_open.core.common import remove_file

# Create the MapNode so that the `remove_file` function handles lists of files
remove_files_node = MapNode(Function(
function = remove_file,
input_names = ['_', 'file_name'],
output_names = []
), name = 'remove_files_node', iterfield = ['file_name'])

# ... A couple of lines later, in the Worlflow definition
test_workflow = Workflow(base_dir = '/home/bclenet/dev/tests/nipype_merge/', name = 'test_workflow')
test_workflow.connect([
# ...
# Here we assume the select_node's output `out_files` is a list of files
(select_node, remove_files_node, [('out_files', 'file_name')])
# ...
])
```

## narps_open.core.common

This module contains a set of functions that nearly every pipeline could use.

* `remove_file` remove a file when it is not needed anymore (to save disk space)

```python
from narps_open.core.common import remove_file

# Remove the /path/to/the/image.nii.gz file
remove_file('/path/to/the/image.nii.gz')
```

* `elements_in_string` : return the first input parameter if it contains one element of second parameter (None otherwise).

```python
from narps_open.core.common import elements_in_string

# Here we test if the file 'sub-001_file.nii.gz' belongs to a group of subjects.
elements_in_string('sub-001_file.nii.gz', ['005', '006', '007']) # Returns None
elements_in_string('sub-001_file.nii.gz', ['001', '002', '003']) # Returns 'sub-001_file.nii.gz'
```

> [!TIP]
> This can be generalised to a group of files, using a `nipype.MapNode`!

* `clean_list` : remove elements of the first input parameter (list) if it is equal to the second parameter.

```python
from narps_open.core.common import clean_list

# Here we remove subject 002 from a group of subjects.
clean_list(['002', '005', '006', '007'], '002')
```

* `list_intersection` : return the intersection of two lists.

```python
from narps_open.core.common import list_intersection

# Here we keep only subjects that are in the equalRange group and selected for the analysis.
equal_range_group = ['002', '004', '006', '008']
selected_for_analysis = ['002', '006', '010']
list_intersection(equal_range_group, selected_for_analysis) # Returns ['002', '006']
```

## narps_open.core.image

This module contains a set of functions dedicated to computations on images.

* `get_voxel_dimensions` : returns the voxel dimensions of an image

```python
# Get dimensions of voxels along x, y, and z in mm (returns e.g.: [1.0, 1.0, 1.0]).
get_voxel_dimensions('/path/to/the/image.nii.gz')
```
Empty file added narps_open/core/__init__.py
Empty file.
65 changes: 65 additions & 0 deletions narps_open/core/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/python
# coding: utf-8

""" Common functions to write pipelines """

def remove_file(_, file_name: str) -> None:
"""
Fully remove files generated by a Node, once they aren't needed anymore.
This function is meant to be used in a Nipype Function Node.

Parameters:
- _: input only used for triggering the Node
- file_name: str, a single absolute filename of the file to remove
"""
# This import must stay inside the function, as required by Nipype
from os import remove

try:
remove(file_name)
except OSError as error:
print(error)

def elements_in_string(input_str: str, elements: list) -> str: #| None:
"""
Return input_str if it contains one element of the elements list.
Return None otherwise.
This function is meant to be used in a Nipype Function Node.

Parameters:
- input_str: str
- elements: list of str, elements to be searched in input_str
"""
if any(e in input_str for e in elements):
return input_str
return None

def clean_list(input_list: list, element = None) -> list:
"""
Remove elements of input_list that are equal to element and return the resultant list.
This function is meant to be used in a Nipype Function Node. It can be used inside a
nipype.Workflow.connect call as well.

Parameters:
- input_list: list
- element: any

Returns:
- input_list with elements equal to element removed
"""
return [f for f in input_list if f != element]

def list_intersection(list_1: list, list_2: list) -> list:
"""
Returns the intersection of two lists.
This function is meant to be used in a Nipype Function Node. It can be used inside a
nipype.Workflow.connect call as well.

Parameters:
- list_1: list
- list_2: list

Returns:
- list, the intersection of list_1 and list_2
"""
return [e for e in list_1 if e in list_2]
25 changes: 25 additions & 0 deletions narps_open/core/image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/python
# coding: utf-8

""" Image functions to write pipelines """

def get_voxel_dimensions(image: str) -> list:
"""
Return the voxel dimensions of a image in millimeters.

Arguments:
image: str, string that represent an absolute path to a Nifti image.

Returns:
list, size of the voxels in the image in millimeters.
"""
# This import must stay inside the function, as required by Nipype
from nibabel import load

voxel_dimensions = load(image).header.get_zooms()

return [
float(voxel_dimensions[0]),
float(voxel_dimensions[1]),
float(voxel_dimensions[2])
]
8 changes: 8 additions & 0 deletions narps_open/data/participants.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,11 @@ def get_participants(team_id: str) -> list:
def get_participants_subset(nb_participants: int = 108) -> list:
""" Return a list of participants of length nb_participants """
return get_all_participants()[0:nb_participants]

def get_group(group_name: str) -> list:
""" Return a list containing all the participants inside the group_name group

Warning : the subject ids are return as written in the participants file (i.e.: 'sub-*')
"""
participants = get_participants_information()
return participants.loc[participants['group'] == group_name]['participant_id'].values.tolist()
Empty file added tests/core/__init__.py
Empty file.
Loading