Skip to content

Commit

Permalink
Common pipeline functions in narps_open.core (Inria-Empenn#128)
Browse files Browse the repository at this point in the history
* [BUG] inside unit_tests workflow

* [ENH] add a remove_files method the new  module

* [TEST] add test for remove_files

* [ENH] add voxel_dimensions [TEST] add test for voxel_dimensions

* Adding sorting utils to narps_open.core.common[skip ci]

* [TEST] for the core functions + adding get_group to narps_open.data.participants

* [DOC] for the core module

* [DOC] codespell
  • Loading branch information
bclenet authored Nov 20, 2023
1 parent 69c7296 commit 83a58ed
Show file tree
Hide file tree
Showing 12 changed files with 627 additions and 12 deletions.
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

0 comments on commit 83a58ed

Please sign in to comment.