diff --git a/.all-contributorsrc b/.all-contributorsrc
index 4abc918..f6bd218 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -25,7 +25,7 @@
"code",
"ideas",
"infra",
- "review",
+ "review"
]
},
{
@@ -38,7 +38,7 @@
"data",
"ideas",
"infra",
- "projectManagement",
+ "projectManagement"
]
},
{
@@ -51,6 +51,20 @@
"review",
"test"
]
+ },
+ {
+ "login": "maestroque",
+ "name": "George Kikas",
+ "avatar_url": "https://avatars.githubusercontent.com/u/74024609?v=4",
+ "profile": "https://github.com/maestroque",
+ "contributions": [
+ "code",
+ "ideas",
+ "infra",
+ "bug",
+ "test",
+ "review"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index de2f6a4..f677eb9 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Ross Markello π» π€ π π |
Stefano Moia π» π£ π€ π π |
Eneko UruΓ±uela π» π β οΈ |
+ George Kikas π» π€ π π β οΈ π |
diff --git a/physutils/io.py b/physutils/io.py
index 76a7f75..3cb8f79 100644
--- a/physutils/io.py
+++ b/physutils/io.py
@@ -9,7 +9,6 @@
import os.path as op
import numpy as np
-from bids import BIDSLayout
from loguru import logger
from physutils import physio
@@ -28,7 +27,7 @@ def load_from_bids(
suffix="physio",
):
"""
- Load physiological data from BIDS-formatted directory
+ Load physiological data from BIDS-formatted directory.
Parameters
----------
@@ -50,6 +49,12 @@ def load_from_bids(
data : :class:`physutils.Physio`
Loaded physiological data
"""
+ try:
+ from bids import BIDSLayout
+ except ImportError:
+ raise ImportError(
+ "To use BIDS-based feature, pybids must be installed. Install manually or with `pip install physutils[bids]`"
+ )
# check if file exists and is in BIDS format
if not op.exists(bids_path):
diff --git a/physutils/tasks.py b/physutils/tasks.py
new file mode 100644
index 0000000..451ac89
--- /dev/null
+++ b/physutils/tasks.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+"""Helper class for holding physiological data and associated metadata information."""
+
+import logging
+
+from .io import load_from_bids, load_physio
+from .physio import Physio
+from .utils import is_bids_directory
+
+# from loguru import logger
+
+try:
+ from pydra.mark import task
+except ImportError:
+ from .utils import task
+
+
+LGR = logging.getLogger(__name__)
+LGR.setLevel(logging.DEBUG)
+
+
+@task
+def generate_physio(
+ input_file: str, mode="auto", fs=None, bids_parameters=dict(), col_physio_type=None
+) -> Physio:
+ """
+ Load a physio object from either a BIDS directory or an exported physio object.
+
+ Parameters
+ ----------
+ input_file : str
+ Path to input file
+ mode : 'auto', 'physio', or 'bids', optional
+ Mode to operate with
+ fs : None, optional
+ Set or force set sapmling frequency (Hz).
+ bids_parameters : dictionary, optional
+ Dictionary containing BIDS parameters
+ col_physio_type : int or None, optional
+ Object to pick up in a BIDS array of physio objects.
+
+ """
+ LGR.info(f"Loading physio object from {input_file}")
+
+ if mode == "auto":
+ if input_file.endswith((".phys", ".physio", ".1D", ".txt", ".tsv", ".csv")):
+ mode = "physio"
+ elif is_bids_directory(input_file):
+ mode = "bids"
+ else:
+ raise ValueError(
+ "Could not determine input mode automatically. Please specify it manually."
+ )
+ if mode == "physio":
+ physio_obj = load_physio(input_file, fs=fs, allow_pickle=True)
+
+ elif mode == "bids":
+ if bids_parameters is {}:
+ raise ValueError("BIDS parameters must be provided when loading from BIDS")
+ else:
+ physio_array = load_from_bids(input_file, **bids_parameters)
+ physio_obj = (
+ physio_array[col_physio_type] if col_physio_type else physio_array
+ )
+ else:
+ raise ValueError(f"Invalid generate_physio mode: {mode}")
+
+ return physio_obj
diff --git a/physutils/tests/data/non-bids-dir/sub-01/ses-01/func/sub-01_ses-01_task-rest_run-01_physio.json b/physutils/tests/data/non-bids-dir/sub-01/ses-01/func/sub-01_ses-01_task-rest_run-01_physio.json
new file mode 100644
index 0000000..68d9a70
--- /dev/null
+++ b/physutils/tests/data/non-bids-dir/sub-01/ses-01/func/sub-01_ses-01_task-rest_run-01_physio.json
@@ -0,0 +1,12 @@
+{
+ "SamplingFrequency": 10000.0,
+ "StartTime": -3,
+ "Columns": [
+ "time",
+ "respiratory_chest",
+ "trigger",
+ "cardiac",
+ "respiratory_CO2",
+ "respiratory_O2"
+ ]
+}
diff --git a/physutils/tests/data/non-bids-dir/sub-01/ses-01/func/sub-01_ses-01_task-rest_run-01_recording-cardiac_physio.json b/physutils/tests/data/non-bids-dir/sub-01/ses-01/func/sub-01_ses-01_task-rest_run-01_recording-cardiac_physio.json
new file mode 100644
index 0000000..68d9a70
--- /dev/null
+++ b/physutils/tests/data/non-bids-dir/sub-01/ses-01/func/sub-01_ses-01_task-rest_run-01_recording-cardiac_physio.json
@@ -0,0 +1,12 @@
+{
+ "SamplingFrequency": 10000.0,
+ "StartTime": -3,
+ "Columns": [
+ "time",
+ "respiratory_chest",
+ "trigger",
+ "cardiac",
+ "respiratory_CO2",
+ "respiratory_O2"
+ ]
+}
diff --git a/physutils/tests/test_tasks.py b/physutils/tests/test_tasks.py
new file mode 100644
index 0000000..4c6284e
--- /dev/null
+++ b/physutils/tests/test_tasks.py
@@ -0,0 +1,107 @@
+"""Tests for physutils.tasks and their integration."""
+
+import os
+
+import physutils.tasks as tasks
+from physutils import physio
+from physutils.tests.utils import create_random_bids_structure
+
+
+def test_generate_physio_phys_file():
+ """Test generate_physio task."""
+ physio_file = os.path.abspath("physutils/tests/data/ECG.phys")
+ task = tasks.generate_physio(input_file=physio_file, mode="physio")
+ assert task.inputs.input_file == physio_file
+ assert task.inputs.mode == "physio"
+ assert task.inputs.fs is None
+
+ task()
+
+ physio_obj = task.result().output.out
+ assert isinstance(physio_obj, physio.Physio)
+ assert physio_obj.fs == 1000
+ assert physio_obj.data.shape == (44611,)
+
+
+def test_generate_physio_bids_file():
+ """Test generate_physio task."""
+ create_random_bids_structure("physutils/tests/data", recording_id="cardiac")
+ bids_parameters = {
+ "subject": "01",
+ "session": "01",
+ "task": "rest",
+ "run": "01",
+ "recording": "cardiac",
+ }
+ bids_dir = os.path.abspath("physutils/tests/data/bids-dir")
+ task = tasks.generate_physio(
+ input_file=bids_dir,
+ mode="bids",
+ bids_parameters=bids_parameters,
+ col_physio_type="cardiac",
+ )
+
+ assert task.inputs.input_file == bids_dir
+ assert task.inputs.mode == "bids"
+ assert task.inputs.fs is None
+ assert task.inputs.bids_parameters == bids_parameters
+ assert task.inputs.col_physio_type == "cardiac"
+
+ task()
+
+ physio_obj = task.result().output.out
+ assert isinstance(physio_obj, physio.Physio)
+
+
+def test_generate_physio_auto():
+ create_random_bids_structure("physutils/tests/data", recording_id="cardiac")
+ bids_parameters = {
+ "subject": "01",
+ "session": "01",
+ "task": "rest",
+ "run": "01",
+ "recording": "cardiac",
+ }
+ bids_dir = os.path.abspath("physutils/tests/data/bids-dir")
+ task = tasks.generate_physio(
+ input_file=bids_dir,
+ mode="auto",
+ bids_parameters=bids_parameters,
+ col_physio_type="cardiac",
+ )
+
+ assert task.inputs.input_file == bids_dir
+ assert task.inputs.mode == "auto"
+ assert task.inputs.fs is None
+ assert task.inputs.bids_parameters == bids_parameters
+ assert task.inputs.col_physio_type == "cardiac"
+
+ task()
+
+ physio_obj = task.result().output.out
+ assert isinstance(physio_obj, physio.Physio)
+
+
+def test_generate_physio_auto_error(caplog):
+ bids_dir = os.path.abspath("physutils/tests/data/non-bids-dir")
+ task = tasks.generate_physio(
+ input_file=bids_dir,
+ mode="auto",
+ col_physio_type="cardiac",
+ )
+
+ assert task.inputs.input_file == bids_dir
+ assert task.inputs.mode == "auto"
+ assert task.inputs.fs is None
+ assert task.inputs.col_physio_type == "cardiac"
+
+ try:
+ task()
+ except Exception:
+ assert caplog.text.count("ERROR") == 1
+ assert (
+ caplog.text.count(
+ "dataset_description.json' is missing from project root. Every valid BIDS dataset must have this file."
+ )
+ == 1
+ )
diff --git a/physutils/utils.py b/physutils/utils.py
new file mode 100644
index 0000000..dd6074f
--- /dev/null
+++ b/physutils/utils.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+"""Helper class for holding physiological data and associated metadata information."""
+
+import logging
+from functools import wraps
+
+from loguru import logger
+
+LGR = logging.getLogger(__name__)
+LGR.setLevel(logging.DEBUG)
+
+
+def task(func):
+ """
+ Fake task decorator to import when pydra is not installed/used.
+
+ Parameters
+ ----------
+ func: function
+ Function to run the wrapper around
+
+ Returns
+ -------
+ function
+ """
+
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ return func(*args, **kwargs)
+ LGR.debug(
+ "Pydra is not installed, thus generate_physio is not available as a pydra task. Using the function directly"
+ )
+
+ return wrapper
+
+
+def is_bids_directory(path_to_dir):
+ """
+ Check if a directory is a BIDS compliant directory.
+
+ Parameters
+ ----------
+ path_to_dir : os.path or str
+ Path to (supposed) BIDS directory
+
+ Returns
+ -------
+ bool
+ True if the given path is a BIDS directory, False is not.
+ """
+ try:
+ from bids import BIDSLayout
+ except ImportError:
+ raise ImportError(
+ "To use BIDS-based feature, pybids must be installed. Install manually or with `pip install physutils[bids]`"
+ )
+ try:
+ # Attempt to create a BIDSLayout object
+ _ = BIDSLayout(path_to_dir)
+ return True
+ except Exception as e:
+ # Catch other exceptions that might indicate the directory isn't BIDS compliant
+ logger.error(
+ f"An error occurred while trying to load {path_to_dir} as a BIDS Layout object: {e}"
+ )
+ return False
diff --git a/setup.cfg b/setup.cfg
index 05d8cb3..619fbf5 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -11,7 +11,7 @@ classifiers =
License :: OSI Approved :: Apache Software License
Programming Language :: Python :: 3
license = Apache-2.0
-description = Set of utilities meant to be used with Physiopy's libraries
+description = Set of utilities meant to be used with Physiopy libraries
long_description = file:README.md
long_description_content_type = text/markdown; charset=UTF-8
platforms = OS Independent
@@ -23,9 +23,7 @@ python_requires = >=3.6.1
install_requires =
matplotlib
numpy >=1.9.3
- scipy
loguru
- pybids
tests_require =
pytest >=3.6
test_suite = pytest
@@ -34,6 +32,10 @@ packages = find:
include_package_data = True
[options.extras_require]
+pydra =
+ pydra
+bids =
+ pybids
doc =
sphinx >=2.0
sphinx-argparse
@@ -49,6 +51,8 @@ test =
scipy
pytest >=5.3
pytest-cov
+ %(pydra)s
+ %(bids)s
%(style)s
devtools =
pre-commit