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

Node creators in narps_open.core.nodes #149

Merged
merged 35 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
6986e90
[BUG] inside unit_tests workflow
bclenet Aug 31, 2023
d6e67f3
Merge branch 'Inria-Empenn:main' into main
bclenet Aug 31, 2023
c3bfc53
Merge branch 'Inria-Empenn:main' into main
bclenet Sep 4, 2023
4b30504
Merge branch 'Inria-Empenn:main' into main
bclenet Sep 19, 2023
fd15ffc
Merge branch 'Inria-Empenn:main' into main
bclenet Sep 21, 2023
6ebe5d2
Merge branch 'Inria-Empenn:main' into main
bclenet Sep 29, 2023
0a584dd
Merge branch 'Inria-Empenn:main' into main
bclenet Sep 29, 2023
e284b80
Merge branch 'Inria-Empenn:main' into main
bclenet Sep 29, 2023
5774813
Merge branch 'Inria-Empenn:main' into main
bclenet Oct 5, 2023
8f12d3d
Merge branch 'Inria-Empenn:main' into main
bclenet Oct 5, 2023
91dc744
Merge branch 'Inria-Empenn:main' into main
bclenet Oct 10, 2023
c03e9d1
Merge branch 'Inria-Empenn:main' into main
bclenet Nov 20, 2023
08dd341
[DOC] runner help
bclenet Nov 22, 2023
fe0d25b
Merge branch 'Inria-Empenn:main' into main
bclenet Nov 22, 2023
04d5ff2
Merge branch 'Inria-Empenn:main' into main
bclenet Nov 22, 2023
c9ee889
Merge branch 'Inria-Empenn:main' into main
bclenet Jan 5, 2024
9bfc89c
Merge branch 'Inria-Empenn:main' into main
bclenet Jan 10, 2024
65c09f7
Merge branch 'main' into runner
bclenet Jan 10, 2024
4c6c894
Creating entry-points for the project
bclenet Jan 10, 2024
0f71b31
[DOC] command line tools
bclenet Jan 10, 2024
7adb57f
[DOC] command line tools
bclenet Jan 10, 2024
13c0393
Adding a tester command line tool
bclenet Jan 10, 2024
7493f74
Merge branch 'Inria-Empenn:main' into main
bclenet Jan 11, 2024
a73f3c3
[DATALAD] change results url
bclenet Jan 11, 2024
f03f50e
Merge pull request #3 from bclenet/results
bclenet Jan 11, 2024
a737df5
Merge branch 'Inria-Empenn:main' into main
bclenet Jan 19, 2024
907f590
Merge branch 'main' into runner
bclenet Jan 24, 2024
fc01b40
Runner configuration
bclenet Jan 24, 2024
f93ae58
Remove dir func in
bclenet Jan 24, 2024
f02e6cc
Runner always stops on first crash
bclenet Jan 24, 2024
060b64f
[TEST][helpers] not failing test if correlation under threshold
bclenet Jan 24, 2024
416ec56
[DOC] narps_open.core.common
bclenet Jan 24, 2024
88b4aa2
[ENH][TEST] narps_open.core.nodes module
bclenet Jan 25, 2024
6f2047c
[ENH][DOC] node generators in core module
bclenet Jan 25, 2024
2cb35a3
[PEP8][SPELL] node generators in core module
bclenet Jan 25, 2024
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
2 changes: 1 addition & 1 deletion .github/workflows/test_changes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,5 @@ jobs:
- name: Execute tests with pytest
run: |
if [[ "${{ needs.identify-tests.outputs.tests }}" != "" ]]; then
pytest -s -q ${{ needs.identify-tests.outputs.tests }}
pytest -s -q ${{ needs.identify-tests.outputs.tests }} -m "not pipeline_test"
fi
52 changes: 52 additions & 0 deletions docs/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ from narps_open.core.common import remove_file
remove_file('/path/to/the/image.nii.gz')
```

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

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

# Remove the directory /path/to/
remove_directory('/path/to/')
```

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

```python
Expand Down Expand Up @@ -115,3 +124,46 @@ This module contains a set of functions dedicated to computations on images.
# 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')
```

## narps_open.core.nodes

This module contains a set of node creators inheriting form the `narps_open.core.nodes.NodeCreator` abstract class.
These are responsible for creating nipype `Node` objects (for now, only based on the `Function` interface, with functions defined in the `narps_open.core.common` module) to be used inside pipeline code. This allows to factorize code, hence making code simpler to read inside pipeline definition.

Here is an example how to use the node creators :

```python
from narps_open.core.nodes import RemoveDirectoryNodeCreator, RemoveFileNodeCreator

# Create a Node to remove a directory
remove_smoothed = RemoveDirectoryNodeCreator.create_node('remove_smoothed')
remove_smoothed.inputs.directory_name = 'my_directory'

# Create a Node to remove a file
remove_gunzip = RemoveFileNodeCreator.create_node('remove_gunzip')
remove_gunzip.inputs.file_name = 'my_file'
```

For your information, this is how an equivalent code would look like without node creators.

```python
from nipype import Node
from nipype.interfaces.utility import Function
from narps_open.core.common import remove_directory, remove_file

# Create a Node to remove a directory
remove_smoothed = Node(Function(
function = remove_directory,
input_names = ['_', 'directory_name'],
output_names = []
), name = 'remove_smoothed')
remove_smoothed.inputs.directory_name = 'my_directory'

# Create a Node to remove a file
remove_gunzip = Node(Function(
function = remove_file,
input_names = ['_', 'file_name'],
output_names = []
), name = 'remove_gunzip')
remove_gunzip.inputs.file_name = 'my_file'
```
14 changes: 14 additions & 0 deletions narps_open/core/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ def remove_file(_, file_name: str) -> None:
except OSError as error:
print(error)

def remove_directory(_, directory_name: str) -> None:
"""
Fully remove directory generated by a Node, once it is not needed anymore.
This function is meant to be used in a Nipype Function Node.

Parameters:
- _: input only used for triggering the Node
- directory_name: str, a single absolute path of the directory to remove
"""
# This import must stay inside the function, as required by Nipype
from shutil import rmtree

rmtree(directory_name, ignore_errors = True)

def elements_in_string(input_str: str, elements: list) -> str: #| None:
"""
Return input_str if it contains one element of the elements list.
Expand Down
44 changes: 44 additions & 0 deletions narps_open/core/nodes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/python
# coding: utf-8

""" Generate useful and recurrent nodes to write pipelines """

from abc import ABC, abstractmethod

from nipype import Node
from nipype.interfaces.utility import Function

from narps_open.core.common import remove_directory, remove_file

class NodeCreator(ABC):
""" An abstract class to shape what node creators must provide """

@staticmethod
@abstractmethod
def create_node(name: str) -> Node:
""" Return a new Node (the interface of the Node is defined by specialized classes)
Arguments:
name, str : the name of the node
"""

class RemoveDirectoryNodeCreator(NodeCreator):
""" A node creator that provides an interface allowing to remove a directory """

@staticmethod
def create_node(name: str) -> Node:
return Node(Function(
function = remove_directory,
input_names = ['_', 'directory_name'],
output_names = []
), name = name)

class RemoveFileNodeCreator(NodeCreator):
""" A node creator that provides an interface allowing to remove a file """

@staticmethod
def create_node(name: str) -> Node:
return Node(Function(
function = remove_file,
input_names = ['_', 'file_name'],
output_names = []
), name = name)
10 changes: 7 additions & 3 deletions narps_open/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from random import choices
from argparse import ArgumentParser

from nipype import Workflow
from nipype import Workflow, config

from narps_open.pipelines import Pipeline, implemented_pipelines
from narps_open.data.participants import (
Expand Down Expand Up @@ -95,6 +95,10 @@ def start(self, first_level_only: bool = False, group_level_only: bool = False)
(= preprocessing + run level + subject_level)
- group_level_only: bool (False by default), run the group level workflows only
"""
# Set global nipype config for pipeline execution
config.update_config(dict(execution = {'stop_on_first_crash': 'True'}))

# Disclaimer
print('Starting pipeline for team: '+
f'{self.team_id}, with {len(self.subjects)} subjects: {self.subjects}')

Expand Down Expand Up @@ -126,15 +130,15 @@ def start(self, first_level_only: bool = False, group_level_only: bool = False)
raise AttributeError('Workflow must be of type nipype.Workflow')

if nb_procs > 1:
sub_workflow.run('MultiProc', plugin_args={'n_procs': nb_procs})
sub_workflow.run('MultiProc', plugin_args = {'n_procs': nb_procs})
else:
sub_workflow.run()
else:
if not isinstance(workflow, Workflow):
raise AttributeError('Workflow must be of type nipype.Workflow')

if nb_procs > 1:
workflow.run('MultiProc', plugin_args={'n_procs': nb_procs})
workflow.run('MultiProc', plugin_args = {'n_procs': nb_procs})
else:
workflow.run()

Expand Down
3 changes: 3 additions & 0 deletions narps_open/utils/configuration/default_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@ narps_results = "data/results/"
[runner]
nb_procs = 8 # Maximum number of threads executed by the runner

[pipelines]
remove_unused_data = true # set to true to activate remove nodes of pipelines

[results]
neurovault_naming = true # true if results files are saved using the neurovault naming, false if they use naming of narps
3 changes: 3 additions & 0 deletions narps_open/utils/configuration/testing_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ test_runs = "run/"
nb_procs = 8 # Maximum number of threads executed by the runner
nb_trials = 3 # Maximum number of executions to have the pipeline executed completely

[pipelines]
remove_unused_data = true # set to true to activate remove nodes of pipelines

[results]
neurovault_naming = true # true if results files are saved using the neurovault naming, false if they use naming of narps

Expand Down
3 changes: 2 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,5 @@ def test_pipeline_evaluation(team_id: str):
file.write('success' if passed else 'failure')
file.write(f' | {[round(i, 2) for i in results]} |\n')

assert passed
if not passed:
break
30 changes: 29 additions & 1 deletion tests/core/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
pytest -q test_common.py
pytest -q test_common.py -k <selected_test>
"""
from os import mkdir
from os import mkdir, makedirs
from os.path import join, exists, abspath
from shutil import rmtree
from pathlib import Path
Expand Down Expand Up @@ -59,6 +59,34 @@ def test_remove_file(remove_test_dir):

# Check file is removed
assert not exists(test_file_path)

@staticmethod
@mark.unit_test
def test_remove_directory(remove_test_dir):
""" Test the remove_directory function """

# Create a single inside dir tree
dir_path = abspath(join(TEMPORARY_DIR, 'dir_1', 'dir_2'))
makedirs(dir_path)
file_path = abspath(join(TEMPORARY_DIR, 'dir_1', 'dir_2', 'file1.txt'))
Path(file_path).touch()
test_dir_path = abspath(join(TEMPORARY_DIR, 'dir_1'))

# Check file exist
assert exists(file_path)

# Create a Nipype Node using remove_files
test_remove_dir_node = Node(Function(
function = co.remove_directory,
input_names = ['_', 'directory_name'],
output_names = []
), name = 'test_remove_dir_node')
test_remove_dir_node.inputs._ = ''
test_remove_dir_node.inputs.directory_name = test_dir_path
test_remove_dir_node.run()

# Check file is removed
assert not exists(test_dir_path)

@staticmethod
@mark.unit_test
Expand Down
68 changes: 68 additions & 0 deletions tests/core/test_nodes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/python
# coding: utf-8

""" Tests of the 'narps_open.core.nodes' module.

Launch this test with PyTest

Usage:
======
pytest -q test_nodes.py
pytest -q test_nodes.py -k <selected_test>
"""

from pytest import mark

from nipype import Node
from nipype.interfaces.utility import Select, Function

from narps_open.core import nodes

class TestNodeCreator:
""" A class that contains all the unit tests for the NodeCreator class."""

@staticmethod
@mark.unit_test
def test_create_node():
""" Test the create_node method """

# Define another child for NodeCreator
class ValidNC(nodes.NodeCreator):
""" A valid implementation of a NodeCreator """

@staticmethod
def create_node(name: str) -> Node:
""" Return a Node, as expected """
return Node(Select(), name = name)

# Test it can be instantiated
test_node = ValidNC.create_node('node_name')
assert isinstance(test_node, Node)
assert isinstance(test_node.interface, Select)
assert test_node.name == 'node_name'

class TestRemoveDirectoryNodeCreator:
""" A class that contains all the unit tests for the RemoveDirectoryNodeCreator class."""

@staticmethod
@mark.unit_test
def test_create_node():
""" Test the create_node method """

test_node = nodes.RemoveDirectoryNodeCreator.create_node('node_name')
assert isinstance(test_node, Node)
assert isinstance(test_node.interface, Function)
assert test_node.name == 'node_name'

class TestRemoveFileNodeCreator:
""" A class that contains all the unit tests for the RemoveFileNodeCreator class."""

@staticmethod
@mark.unit_test
def test_create_node():
""" Test the create_node method """

test_node = nodes.RemoveFileNodeCreator.create_node('node_name')
assert isinstance(test_node, Node)
assert isinstance(test_node.interface, Function)
assert test_node.name == 'node_name'
Loading