Skip to content

Commit ab85017

Browse files
authored
Merge pull request #7 from maestroque/add-pydra
Add BIDS reading support and prepare input loading for pydra workflow
2 parents 7159c4d + f2a2d92 commit ab85017

9 files changed

+300
-7
lines changed

.all-contributorsrc

+16-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"code",
2626
"ideas",
2727
"infra",
28-
"review",
28+
"review"
2929
]
3030
},
3131
{
@@ -38,7 +38,7 @@
3838
"data",
3939
"ideas",
4040
"infra",
41-
"projectManagement",
41+
"projectManagement"
4242
]
4343
},
4444
{
@@ -51,6 +51,20 @@
5151
"review",
5252
"test"
5353
]
54+
},
55+
{
56+
"login": "maestroque",
57+
"name": "George Kikas",
58+
"avatar_url": "https://avatars.githubusercontent.com/u/74024609?v=4",
59+
"profile": "https://github.com/maestroque",
60+
"contributions": [
61+
"code",
62+
"ideas",
63+
"infra",
64+
"bug",
65+
"test",
66+
"review"
67+
]
5468
}
5569
],
5670
"contributorsPerLine": 7,

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
4040
<td align="center"><a href="http://rossmarkello.com"><img src="https://avatars0.githubusercontent.com/u/14265705?v=4" width="100px;" alt=""/><br /><sub><b>Ross Markello</b></sub></a><br /><a href="https://github.com/physiopy/phys2bids/commits?author=rmarkello" title="Code">💻</a> <a href="#ideas-rmarkello" title="Ideas, Planning, & Feedback">🤔</a> <a href="#infra-rmarkello" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/physiopy/phys2bids/pulls?q=is%3Apr+reviewed-by%3Armarkello" title="Reviewed Pull Requests">👀</a></td>
4141
<td align="center"><a href="https://github.com/smoia"><img src="https://avatars3.githubusercontent.com/u/35300580?v=4" width="100px;" alt=""/><br /><sub><b>Stefano Moia</b></sub></a><br /><a href="https://github.com/physiopy/phys2bids/commits?author=smoia" title="Code">💻</a> <a href="#data-smoia" title="Data">🔣</a> <a href="#ideas-smoia" title="Ideas, Planning, & Feedback">🤔</a> <a href="#infra-smoia" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#projectManagement-smoia" title="Project Management">📆</a></td>
4242
<td align="center"><a href="https://github.com/eurunuela"><img src="https://avatars0.githubusercontent.com/u/13706448?v=4" width="100px;" alt=""/><br /><sub><b>Eneko Uruñuela</b></sub></a><br /><a href="https://github.com/physiopy/phys2bids/commits?author=eurunuela" title="Code">💻</a> <a href="https://github.com/physiopy/phys2bids/pulls?q=is%3Apr+reviewed-by%3Aeurunuela" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/physiopy/phys2bids/commits?author=eurunuela" title="Tests">⚠️</a></td>
43+
<td align="center"><a href="https://github.com/maestroque"><img src="https://avatars.githubusercontent.com/u/74024609?v=4?s=100" width="100px;" alt="George Kikas"/><br /><sub><b>George Kikas</b></sub></a><br /><a href="https://github.com/physiopy/phys2denoise/commits?author=maestroque" title="Code">💻</a> <a href="#ideas-maestroque" title="Ideas, Planning, & Feedback">🤔</a> <a href="#infra-maestroque" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/physiopy/phys2denoise/issues?q=author%3Amaestroque" title="Bug reports">🐛</a> <a href="https://github.com/physiopy/phys2denoise/commits?author=maestroque" title="Tests">⚠️</a> <a href="https://github.com/physiopy/phys2denoise/pulls?q=is%3Apr+reviewed-by%3Amaestroque" title="Reviewed Pull Requests">👀</a></td>
4344
</tr>
4445
</table>
4546

physutils/io.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import os.path as op
1010

1111
import numpy as np
12-
from bids import BIDSLayout
1312
from loguru import logger
1413

1514
from physutils import physio
@@ -28,7 +27,7 @@ def load_from_bids(
2827
suffix="physio",
2928
):
3029
"""
31-
Load physiological data from BIDS-formatted directory
30+
Load physiological data from BIDS-formatted directory.
3231
3332
Parameters
3433
----------
@@ -50,6 +49,12 @@ def load_from_bids(
5049
data : :class:`physutils.Physio`
5150
Loaded physiological data
5251
"""
52+
try:
53+
from bids import BIDSLayout
54+
except ImportError:
55+
raise ImportError(
56+
"To use BIDS-based feature, pybids must be installed. Install manually or with `pip install physutils[bids]`"
57+
)
5358

5459
# check if file exists and is in BIDS format
5560
if not op.exists(bids_path):

physutils/tasks.py

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
"""Helper class for holding physiological data and associated metadata information."""
5+
6+
import logging
7+
8+
from .io import load_from_bids, load_physio
9+
from .physio import Physio
10+
from .utils import is_bids_directory
11+
12+
# from loguru import logger
13+
14+
try:
15+
from pydra.mark import task
16+
except ImportError:
17+
from .utils import task
18+
19+
20+
LGR = logging.getLogger(__name__)
21+
LGR.setLevel(logging.DEBUG)
22+
23+
24+
@task
25+
def generate_physio(
26+
input_file: str, mode="auto", fs=None, bids_parameters=dict(), col_physio_type=None
27+
) -> Physio:
28+
"""
29+
Load a physio object from either a BIDS directory or an exported physio object.
30+
31+
Parameters
32+
----------
33+
input_file : str
34+
Path to input file
35+
mode : 'auto', 'physio', or 'bids', optional
36+
Mode to operate with
37+
fs : None, optional
38+
Set or force set sapmling frequency (Hz).
39+
bids_parameters : dictionary, optional
40+
Dictionary containing BIDS parameters
41+
col_physio_type : int or None, optional
42+
Object to pick up in a BIDS array of physio objects.
43+
44+
"""
45+
LGR.info(f"Loading physio object from {input_file}")
46+
47+
if mode == "auto":
48+
if input_file.endswith((".phys", ".physio", ".1D", ".txt", ".tsv", ".csv")):
49+
mode = "physio"
50+
elif is_bids_directory(input_file):
51+
mode = "bids"
52+
else:
53+
raise ValueError(
54+
"Could not determine input mode automatically. Please specify it manually."
55+
)
56+
if mode == "physio":
57+
physio_obj = load_physio(input_file, fs=fs, allow_pickle=True)
58+
59+
elif mode == "bids":
60+
if bids_parameters is {}:
61+
raise ValueError("BIDS parameters must be provided when loading from BIDS")
62+
else:
63+
physio_array = load_from_bids(input_file, **bids_parameters)
64+
physio_obj = (
65+
physio_array[col_physio_type] if col_physio_type else physio_array
66+
)
67+
else:
68+
raise ValueError(f"Invalid generate_physio mode: {mode}")
69+
70+
return physio_obj
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"SamplingFrequency": 10000.0,
3+
"StartTime": -3,
4+
"Columns": [
5+
"time",
6+
"respiratory_chest",
7+
"trigger",
8+
"cardiac",
9+
"respiratory_CO2",
10+
"respiratory_O2"
11+
]
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"SamplingFrequency": 10000.0,
3+
"StartTime": -3,
4+
"Columns": [
5+
"time",
6+
"respiratory_chest",
7+
"trigger",
8+
"cardiac",
9+
"respiratory_CO2",
10+
"respiratory_O2"
11+
]
12+
}

physutils/tests/test_tasks.py

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""Tests for physutils.tasks and their integration."""
2+
3+
import os
4+
5+
import physutils.tasks as tasks
6+
from physutils import physio
7+
from physutils.tests.utils import create_random_bids_structure
8+
9+
10+
def test_generate_physio_phys_file():
11+
"""Test generate_physio task."""
12+
physio_file = os.path.abspath("physutils/tests/data/ECG.phys")
13+
task = tasks.generate_physio(input_file=physio_file, mode="physio")
14+
assert task.inputs.input_file == physio_file
15+
assert task.inputs.mode == "physio"
16+
assert task.inputs.fs is None
17+
18+
task()
19+
20+
physio_obj = task.result().output.out
21+
assert isinstance(physio_obj, physio.Physio)
22+
assert physio_obj.fs == 1000
23+
assert physio_obj.data.shape == (44611,)
24+
25+
26+
def test_generate_physio_bids_file():
27+
"""Test generate_physio task."""
28+
create_random_bids_structure("physutils/tests/data", recording_id="cardiac")
29+
bids_parameters = {
30+
"subject": "01",
31+
"session": "01",
32+
"task": "rest",
33+
"run": "01",
34+
"recording": "cardiac",
35+
}
36+
bids_dir = os.path.abspath("physutils/tests/data/bids-dir")
37+
task = tasks.generate_physio(
38+
input_file=bids_dir,
39+
mode="bids",
40+
bids_parameters=bids_parameters,
41+
col_physio_type="cardiac",
42+
)
43+
44+
assert task.inputs.input_file == bids_dir
45+
assert task.inputs.mode == "bids"
46+
assert task.inputs.fs is None
47+
assert task.inputs.bids_parameters == bids_parameters
48+
assert task.inputs.col_physio_type == "cardiac"
49+
50+
task()
51+
52+
physio_obj = task.result().output.out
53+
assert isinstance(physio_obj, physio.Physio)
54+
55+
56+
def test_generate_physio_auto():
57+
create_random_bids_structure("physutils/tests/data", recording_id="cardiac")
58+
bids_parameters = {
59+
"subject": "01",
60+
"session": "01",
61+
"task": "rest",
62+
"run": "01",
63+
"recording": "cardiac",
64+
}
65+
bids_dir = os.path.abspath("physutils/tests/data/bids-dir")
66+
task = tasks.generate_physio(
67+
input_file=bids_dir,
68+
mode="auto",
69+
bids_parameters=bids_parameters,
70+
col_physio_type="cardiac",
71+
)
72+
73+
assert task.inputs.input_file == bids_dir
74+
assert task.inputs.mode == "auto"
75+
assert task.inputs.fs is None
76+
assert task.inputs.bids_parameters == bids_parameters
77+
assert task.inputs.col_physio_type == "cardiac"
78+
79+
task()
80+
81+
physio_obj = task.result().output.out
82+
assert isinstance(physio_obj, physio.Physio)
83+
84+
85+
def test_generate_physio_auto_error(caplog):
86+
bids_dir = os.path.abspath("physutils/tests/data/non-bids-dir")
87+
task = tasks.generate_physio(
88+
input_file=bids_dir,
89+
mode="auto",
90+
col_physio_type="cardiac",
91+
)
92+
93+
assert task.inputs.input_file == bids_dir
94+
assert task.inputs.mode == "auto"
95+
assert task.inputs.fs is None
96+
assert task.inputs.col_physio_type == "cardiac"
97+
98+
try:
99+
task()
100+
except Exception:
101+
assert caplog.text.count("ERROR") == 1
102+
assert (
103+
caplog.text.count(
104+
"dataset_description.json' is missing from project root. Every valid BIDS dataset must have this file."
105+
)
106+
== 1
107+
)

physutils/utils.py

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
"""Helper class for holding physiological data and associated metadata information."""
5+
6+
import logging
7+
from functools import wraps
8+
9+
from loguru import logger
10+
11+
LGR = logging.getLogger(__name__)
12+
LGR.setLevel(logging.DEBUG)
13+
14+
15+
def task(func):
16+
"""
17+
Fake task decorator to import when pydra is not installed/used.
18+
19+
Parameters
20+
----------
21+
func: function
22+
Function to run the wrapper around
23+
24+
Returns
25+
-------
26+
function
27+
"""
28+
29+
@wraps(func)
30+
def wrapper(*args, **kwargs):
31+
return func(*args, **kwargs)
32+
LGR.debug(
33+
"Pydra is not installed, thus generate_physio is not available as a pydra task. Using the function directly"
34+
)
35+
36+
return wrapper
37+
38+
39+
def is_bids_directory(path_to_dir):
40+
"""
41+
Check if a directory is a BIDS compliant directory.
42+
43+
Parameters
44+
----------
45+
path_to_dir : os.path or str
46+
Path to (supposed) BIDS directory
47+
48+
Returns
49+
-------
50+
bool
51+
True if the given path is a BIDS directory, False is not.
52+
"""
53+
try:
54+
from bids import BIDSLayout
55+
except ImportError:
56+
raise ImportError(
57+
"To use BIDS-based feature, pybids must be installed. Install manually or with `pip install physutils[bids]`"
58+
)
59+
try:
60+
# Attempt to create a BIDSLayout object
61+
_ = BIDSLayout(path_to_dir)
62+
return True
63+
except Exception as e:
64+
# Catch other exceptions that might indicate the directory isn't BIDS compliant
65+
logger.error(
66+
f"An error occurred while trying to load {path_to_dir} as a BIDS Layout object: {e}"
67+
)
68+
return False

setup.cfg

+7-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ classifiers =
1111
License :: OSI Approved :: Apache Software License
1212
Programming Language :: Python :: 3
1313
license = Apache-2.0
14-
description = Set of utilities meant to be used with Physiopy's libraries
14+
description = Set of utilities meant to be used with Physiopy libraries
1515
long_description = file:README.md
1616
long_description_content_type = text/markdown; charset=UTF-8
1717
platforms = OS Independent
@@ -23,9 +23,7 @@ python_requires = >=3.6.1
2323
install_requires =
2424
matplotlib
2525
numpy >=1.9.3
26-
scipy
2726
loguru
28-
pybids
2927
tests_require =
3028
pytest >=3.6
3129
test_suite = pytest
@@ -34,6 +32,10 @@ packages = find:
3432
include_package_data = True
3533

3634
[options.extras_require]
35+
pydra =
36+
pydra
37+
bids =
38+
pybids
3739
doc =
3840
sphinx >=2.0
3941
sphinx-argparse
@@ -49,6 +51,8 @@ test =
4951
scipy
5052
pytest >=5.3
5153
pytest-cov
54+
%(pydra)s
55+
%(bids)s
5256
%(style)s
5357
devtools =
5458
pre-commit

0 commit comments

Comments
 (0)