From 557513ece91ed07a012e85570824af6c94e0f57d Mon Sep 17 00:00:00 2001 From: maestroque Date: Mon, 22 Jul 2024 16:05:41 +0300 Subject: [PATCH 01/15] Initial pydra transformation task --- physutils/tasks.py | 10 ++++++++++ setup.cfg | 1 + 2 files changed, 11 insertions(+) create mode 100644 physutils/tasks.py diff --git a/physutils/tasks.py b/physutils/tasks.py new file mode 100644 index 0000000..dfc65a4 --- /dev/null +++ b/physutils/tasks.py @@ -0,0 +1,10 @@ +import pydra + +from physutils.io import load_physio +from physutils.physio import Physio + + +@pydra.mark.task +def transform_to_physio(input_file: str) -> Physio: + physio_obj = load_physio(input_file) + return physio_obj diff --git a/setup.cfg b/setup.cfg index df85b76..85d6c2f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,6 +25,7 @@ install_requires = numpy >=1.9.3 scipy loguru + pydra tests_require = pytest >=3.6 test_suite = pytest From 52cc61e8bb525cb141b549e4d664a69b0a8fb706 Mon Sep 17 00:00:00 2001 From: maestroque Date: Mon, 22 Jul 2024 16:53:37 +0300 Subject: [PATCH 02/15] Update pydra task --- physutils/tasks.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/physutils/tasks.py b/physutils/tasks.py index dfc65a4..92a0351 100644 --- a/physutils/tasks.py +++ b/physutils/tasks.py @@ -1,10 +1,18 @@ import pydra -from physutils.io import load_physio +from physutils.io import load_history, load_physio from physutils.physio import Physio @pydra.mark.task -def transform_to_physio(input_file: str) -> Physio: - physio_obj = load_physio(input_file) +def transform_to_physio(input_file: str, mode="physio") -> Physio: + if mode == "physio": + physio_obj = load_physio(input_file) + elif mode == "history": + physio_obj = load_history(input_file) + elif mode == "bids": + # TODO: Implement BIDS loading once the bids-support branch is merged + raise NotImplementedError("BIDS loading is not yet implemented") + else: + raise ValueError(f"Invalid transform_to_physio mode: {mode}") return physio_obj From 05d90eac5e6b5ff5755c7abc4d495e1d86d00fe9 Mon Sep 17 00:00:00 2001 From: maestroque Date: Sun, 25 Aug 2024 17:56:24 +0300 Subject: [PATCH 03/15] Add BIDS loading option to transform_to_physio task --- physutils/tasks.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/physutils/tasks.py b/physutils/tasks.py index 92a0351..9c017fd 100644 --- a/physutils/tasks.py +++ b/physutils/tasks.py @@ -1,6 +1,6 @@ import pydra -from physutils.io import load_history, load_physio +from physutils.io import load_from_bids, load_history, load_physio from physutils.physio import Physio @@ -11,8 +11,7 @@ def transform_to_physio(input_file: str, mode="physio") -> Physio: elif mode == "history": physio_obj = load_history(input_file) elif mode == "bids": - # TODO: Implement BIDS loading once the bids-support branch is merged - raise NotImplementedError("BIDS loading is not yet implemented") + physio_obj = load_from_bids(input_file) else: raise ValueError(f"Invalid transform_to_physio mode: {mode}") return physio_obj From cb67b7cb3e7c9c186793ba287d9ac9bad1d5d478 Mon Sep 17 00:00:00 2001 From: maestroque Date: Mon, 26 Aug 2024 20:16:03 +0300 Subject: [PATCH 04/15] Working transform_to_physio pydra task and tests --- physutils/physio.py | 3 ++- physutils/tasks.py | 21 ++++++++++++++++----- physutils/tests/test_tasks.py | 22 ++++++++++++++++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 physutils/tests/test_tasks.py diff --git a/physutils/physio.py b/physutils/physio.py index f557001..42afb9e 100644 --- a/physutils/physio.py +++ b/physutils/physio.py @@ -8,6 +8,7 @@ import matplotlib.pyplot as plt import numpy as np +import pandas as pd from loguru import logger @@ -146,7 +147,7 @@ def check_physio(data, ensure_fs=True, copy=False): if not isinstance(data, Physio): data = load_physio(data) - if ensure_fs and np.isnan(data.fs): + if ensure_fs and pd.isna(data.fs): raise ValueError("Provided data does not have valid sampling rate.") if copy is True: return new_physio_like( diff --git a/physutils/tasks.py b/physutils/tasks.py index 9c017fd..587ca73 100644 --- a/physutils/tasks.py +++ b/physutils/tasks.py @@ -1,15 +1,26 @@ +import logging + import pydra -from physutils.io import load_from_bids, load_history, load_physio +from physutils.io import load_from_bids, load_physio from physutils.physio import Physio +LGR = logging.getLogger(__name__) +LGR.setLevel(logging.DEBUG) + @pydra.mark.task -def transform_to_physio(input_file: str, mode="physio") -> Physio: +def transform_to_physio(input_file: str, mode="physio", fs=None) -> Physio: + LGR.debug(f"Loading physio object from {input_file}") + if not fs: + fs = None + if mode == "physio": - physio_obj = load_physio(input_file) - elif mode == "history": - physio_obj = load_history(input_file) + if fs is not None: + physio_obj = load_physio(input_file, fs=fs, allow_pickle=True) + else: + physio_obj = load_physio(input_file, allow_pickle=True) + elif mode == "bids": physio_obj = load_from_bids(input_file) else: diff --git a/physutils/tests/test_tasks.py b/physutils/tests/test_tasks.py new file mode 100644 index 0000000..6d648d7 --- /dev/null +++ b/physutils/tests/test_tasks.py @@ -0,0 +1,22 @@ +"""Tests for physutils.tasks and their integration.""" + +import os + +import physutils.tasks as tasks +from physutils import physio + + +def test_transform_to_physio_phys_file(): + """Test transform_to_physio task.""" + physio_file = os.path.abspath("physutils/tests/data/ECG.phys") + task = tasks.transform_to_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,) From e10c9b2a04b8b4be68dd8613e4b02a78f0a903fc Mon Sep 17 00:00:00 2001 From: maestroque Date: Tue, 27 Aug 2024 20:13:10 +0300 Subject: [PATCH 05/15] Verify transform_to_physio for BIDS files --- physutils/tasks.py | 10 ++++++++-- physutils/tests/test_tasks.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/physutils/tasks.py b/physutils/tasks.py index 587ca73..084d942 100644 --- a/physutils/tasks.py +++ b/physutils/tasks.py @@ -10,7 +10,9 @@ @pydra.mark.task -def transform_to_physio(input_file: str, mode="physio", fs=None) -> Physio: +def transform_to_physio( + input_file: str, mode="physio", fs=None, bids_parameters=dict(), bids_channel=None +) -> Physio: LGR.debug(f"Loading physio object from {input_file}") if not fs: fs = None @@ -22,7 +24,11 @@ def transform_to_physio(input_file: str, mode="physio", fs=None) -> Physio: physio_obj = load_physio(input_file, allow_pickle=True) elif mode == "bids": - physio_obj = load_from_bids(input_file) + 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[bids_channel] else: raise ValueError(f"Invalid transform_to_physio mode: {mode}") return physio_obj diff --git a/physutils/tests/test_tasks.py b/physutils/tests/test_tasks.py index 6d648d7..8198a91 100644 --- a/physutils/tests/test_tasks.py +++ b/physutils/tests/test_tasks.py @@ -4,6 +4,7 @@ import physutils.tasks as tasks from physutils import physio +from physutils.tests.utils import create_random_bids_structure def test_transform_to_physio_phys_file(): @@ -20,3 +21,33 @@ def test_transform_to_physio_phys_file(): assert isinstance(physio_obj, physio.Physio) assert physio_obj.fs == 1000 assert physio_obj.data.shape == (44611,) + + +def test_transform_to_physio_bids_file(): + """Test transform_to_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.transform_to_physio( + input_file=bids_dir, + mode="bids", + bids_parameters=bids_parameters, + bids_channel="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.bids_channel == "cardiac" + + task() + + physio_obj = task.result().output.out + assert isinstance(physio_obj, physio.Physio) From 717ff0e9b1e8c9fd4a63fd59637d0d003700d847 Mon Sep 17 00:00:00 2001 From: maestroque Date: Wed, 28 Aug 2024 17:26:19 +0300 Subject: [PATCH 06/15] Minor review fixes --- physutils/physio.py | 3 +-- setup.cfg | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/physutils/physio.py b/physutils/physio.py index 42afb9e..f557001 100644 --- a/physutils/physio.py +++ b/physutils/physio.py @@ -8,7 +8,6 @@ import matplotlib.pyplot as plt import numpy as np -import pandas as pd from loguru import logger @@ -147,7 +146,7 @@ def check_physio(data, ensure_fs=True, copy=False): if not isinstance(data, Physio): data = load_physio(data) - if ensure_fs and pd.isna(data.fs): + if ensure_fs and np.isnan(data.fs): raise ValueError("Provided data does not have valid sampling rate.") if copy is True: return new_physio_like( diff --git a/setup.cfg b/setup.cfg index 4c90181..86e5486 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,7 +23,6 @@ python_requires = >=3.6.1 install_requires = matplotlib numpy >=1.9.3 - scipy loguru pydra pybids From db569543a7fcf93998f5bb5e8336d8e52c0e604a Mon Sep 17 00:00:00 2001 From: maestroque Date: Thu, 29 Aug 2024 14:11:22 +0300 Subject: [PATCH 07/15] Add @maestroque as a contributor --- .all-contributorsrc | 18 ++++++++++++++++-- README.md | 1 + 2 files changed, 17 insertions(+), 2 deletions(-) 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..231c066 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
George Kikas

πŸ’» πŸ€” πŸš‡ πŸ› ⚠️ πŸ‘€ From 6c9a6d2652055e02b2f43f0c5abd5ba7615ba63a Mon Sep 17 00:00:00 2001 From: maestroque Date: Thu, 29 Aug 2024 14:13:06 +0300 Subject: [PATCH 08/15] Style fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 231c066..f677eb9 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Ross Markello

πŸ’» πŸ€” πŸš‡ πŸ‘€
Stefano Moia

πŸ’» πŸ”£ πŸ€” πŸš‡ πŸ“†
Eneko UruΓ±uela

πŸ’» πŸ‘€ ⚠️ - George Kikas
George Kikas

πŸ’» πŸ€” πŸš‡ πŸ› ⚠️ πŸ‘€ + George Kikas
George Kikas

πŸ’» πŸ€” πŸš‡ πŸ› ⚠️ πŸ‘€ From 67ee7896021f862f90b60f36c210e79aaf19749d Mon Sep 17 00:00:00 2001 From: maestroque Date: Tue, 3 Sep 2024 16:53:57 +0300 Subject: [PATCH 09/15] Add wrapper for pydra tasks, in order to eliminate pydra requirement --- physutils/tasks.py | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/physutils/tasks.py b/physutils/tasks.py index 084d942..1589164 100644 --- a/physutils/tasks.py +++ b/physutils/tasks.py @@ -1,6 +1,7 @@ import logging +from functools import wraps -import pydra +from loguru import logger from physutils.io import load_from_bids, load_physio from physutils.physio import Physio @@ -8,8 +9,46 @@ LGR = logging.getLogger(__name__) LGR.setLevel(logging.DEBUG) +try: + import pydra -@pydra.mark.task + pydra_imported = True +except ImportError: + logger.warning( + "Pydra is not installed, so the physutils tasks are not available as pydra tasks" + ) + LGR.warning( + "Pydra is not installed, so the physutils tasks are not available as pydra tasks" + ) + pydra_imported = False + + +def mark_task(pydra_imported=pydra_imported): + def decorator(func): + if pydra_imported: + # If the decorator exists, apply it + @wraps(func) + def wrapped_func(*args, **kwargs): + return pydra.mark.task(func)(*args, **kwargs) + + return wrapped_func + # Otherwise, return the original function + logger.warning( + "Pydra is not installed, so {} is not available as a pydra task".format( + func.__name__ + ) + ) + LGR.warning( + "Pydra is not installed, so {} is not available as a pydra task".format( + func.__name__ + ) + ) + return func + + return decorator + + +@mark_task(pydra_imported=pydra_imported) def transform_to_physio( input_file: str, mode="physio", fs=None, bids_parameters=dict(), bids_channel=None ) -> Physio: From 3a08510629dea413bbf538ece0b416ba8bbabe66 Mon Sep 17 00:00:00 2001 From: maestroque Date: Tue, 3 Sep 2024 17:03:27 +0300 Subject: [PATCH 10/15] Add better logs for pydra package import handling --- physutils/tasks.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/physutils/tasks.py b/physutils/tasks.py index 1589164..f34ced7 100644 --- a/physutils/tasks.py +++ b/physutils/tasks.py @@ -14,12 +14,6 @@ pydra_imported = True except ImportError: - logger.warning( - "Pydra is not installed, so the physutils tasks are not available as pydra tasks" - ) - LGR.warning( - "Pydra is not installed, so the physutils tasks are not available as pydra tasks" - ) pydra_imported = False @@ -29,20 +23,11 @@ def decorator(func): # If the decorator exists, apply it @wraps(func) def wrapped_func(*args, **kwargs): + logger.debug(f"Creating pydra task for {func.__name__}") return pydra.mark.task(func)(*args, **kwargs) return wrapped_func # Otherwise, return the original function - logger.warning( - "Pydra is not installed, so {} is not available as a pydra task".format( - func.__name__ - ) - ) - LGR.warning( - "Pydra is not installed, so {} is not available as a pydra task".format( - func.__name__ - ) - ) return func return decorator @@ -52,6 +37,10 @@ def wrapped_func(*args, **kwargs): def transform_to_physio( input_file: str, mode="physio", fs=None, bids_parameters=dict(), bids_channel=None ) -> Physio: + if not pydra_imported: + LGR.warning( + "Pydra is not installed, thus transform_to_physio is not available as a pydra task. Using the function directly" + ) LGR.debug(f"Loading physio object from {input_file}") if not fs: fs = None From 1cd7539e3b96042ae51547ad6070b0ee4bdc0531 Mon Sep 17 00:00:00 2001 From: maestroque Date: Tue, 3 Sep 2024 17:24:16 +0300 Subject: [PATCH 11/15] Add transform_to_physio auto mode --- physutils/tasks.py | 23 ++++++++ ...sub-01_ses-01_task-rest_run-01_physio.json | 12 +++++ ...-rest_run-01_recording-cardiac_physio.json | 12 +++++ physutils/tests/test_tasks.py | 54 +++++++++++++++++++ 4 files changed, 101 insertions(+) create mode 100644 physutils/tests/data/non-bids-dir/sub-01/ses-01/func/sub-01_ses-01_task-rest_run-01_physio.json create mode 100644 physutils/tests/data/non-bids-dir/sub-01/ses-01/func/sub-01_ses-01_task-rest_run-01_recording-cardiac_physio.json diff --git a/physutils/tasks.py b/physutils/tasks.py index f34ced7..7dfb306 100644 --- a/physutils/tasks.py +++ b/physutils/tasks.py @@ -1,6 +1,7 @@ import logging from functools import wraps +from bids import BIDSLayout from loguru import logger from physutils.io import load_from_bids, load_physio @@ -33,6 +34,19 @@ def wrapped_func(*args, **kwargs): return decorator +def is_bids_directory(directory): + try: + # Attempt to create a BIDSLayout object + _ = BIDSLayout(directory) + 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 {directory} as a BIDS Layout object: {e}" + ) + return False + + @mark_task(pydra_imported=pydra_imported) def transform_to_physio( input_file: str, mode="physio", fs=None, bids_parameters=dict(), bids_channel=None @@ -45,6 +59,15 @@ def transform_to_physio( if not fs: fs = None + 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 mode automatically, please specify mode" + ) if mode == "physio": if fs is not None: physio_obj = load_physio(input_file, fs=fs, allow_pickle=True) 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 index 8198a91..9ecdc0b 100644 --- a/physutils/tests/test_tasks.py +++ b/physutils/tests/test_tasks.py @@ -51,3 +51,57 @@ def test_transform_to_physio_bids_file(): physio_obj = task.result().output.out assert isinstance(physio_obj, physio.Physio) + + +def test_transform_to_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.transform_to_physio( + input_file=bids_dir, + mode="auto", + bids_parameters=bids_parameters, + bids_channel="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.bids_channel == "cardiac" + + task() + + physio_obj = task.result().output.out + assert isinstance(physio_obj, physio.Physio) + + +def test_transform_to_physio_auto_error(caplog): + bids_dir = os.path.abspath("physutils/tests/data/non-bids-dir") + task = tasks.transform_to_physio( + input_file=bids_dir, + mode="auto", + bids_channel="cardiac", + ) + + assert task.inputs.input_file == bids_dir + assert task.inputs.mode == "auto" + assert task.inputs.fs is None + assert task.inputs.bids_channel == "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 + ) From a19bc196200bda216e02ed1f772e83c38de16117 Mon Sep 17 00:00:00 2001 From: Stefano Moia Date: Wed, 9 Oct 2024 21:13:32 +0200 Subject: [PATCH 12/15] Create `utils` and move there pydra checks and BIDS directory checks --- physutils/utils.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 physutils/utils.py 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 From 34fc89d9903d240fddf200ae29d0eac0d7d866c7 Mon Sep 17 00:00:00 2001 From: Stefano Moia Date: Wed, 9 Oct 2024 21:15:29 +0200 Subject: [PATCH 13/15] Clean code, remove moved functions, remove pydra mask check, add docstrings, rename transform_to_physio function --- physutils/tasks.py | 94 +++++++++++++++-------------------- physutils/tests/test_tasks.py | 20 ++++---- 2 files changed, 50 insertions(+), 64 deletions(-) diff --git a/physutils/tasks.py b/physutils/tasks.py index 7dfb306..3da6c56 100644 --- a/physutils/tasks.py +++ b/physutils/tasks.py @@ -1,63 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +"""Helper class for holding physiological data and associated metadata information.""" + import logging -from functools import wraps -from bids import BIDSLayout -from loguru import logger +from .io import load_from_bids, load_physio +from .physio import Physio +from .utils import is_bids_directory -from physutils.io import load_from_bids, load_physio -from physutils.physio import Physio +# from loguru import logger -LGR = logging.getLogger(__name__) -LGR.setLevel(logging.DEBUG) try: - import pydra - - pydra_imported = True + from pydra import task except ImportError: - pydra_imported = False - - -def mark_task(pydra_imported=pydra_imported): - def decorator(func): - if pydra_imported: - # If the decorator exists, apply it - @wraps(func) - def wrapped_func(*args, **kwargs): - logger.debug(f"Creating pydra task for {func.__name__}") - return pydra.mark.task(func)(*args, **kwargs) + from .utils import task - return wrapped_func - # Otherwise, return the original function - return func - return decorator +LGR = logging.getLogger(__name__) +LGR.setLevel(logging.DEBUG) -def is_bids_directory(directory): - try: - # Attempt to create a BIDSLayout object - _ = BIDSLayout(directory) - 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 {directory} as a BIDS Layout object: {e}" - ) - return False +@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. -@mark_task(pydra_imported=pydra_imported) -def transform_to_physio( - input_file: str, mode="physio", fs=None, bids_parameters=dict(), bids_channel=None -) -> Physio: - if not pydra_imported: - LGR.warning( - "Pydra is not installed, thus transform_to_physio is not available as a pydra task. Using the function directly" - ) - LGR.debug(f"Loading physio object from {input_file}") - if not fs: - fs = None + """ + LGR.info(f"Loading physio object from {input_file}") if mode == "auto": if input_file.endswith((".phys", ".physio", ".1D", ".txt", ".tsv", ".csv")): @@ -66,20 +52,20 @@ def transform_to_physio( mode = "bids" else: raise ValueError( - "Could not determine mode automatically, please specify mode" + "Could not determine input mode automatically. Please specify it manually." ) if mode == "physio": - if fs is not None: - physio_obj = load_physio(input_file, fs=fs, allow_pickle=True) - else: - physio_obj = load_physio(input_file, allow_pickle=True) + 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[bids_channel] + physio_obj = ( + physio_array[col_physio_type] if col_physio_type else physio_array + ) else: - raise ValueError(f"Invalid transform_to_physio mode: {mode}") + raise ValueError(f"Invalid generate_physio mode: {mode}") + return physio_obj diff --git a/physutils/tests/test_tasks.py b/physutils/tests/test_tasks.py index 9ecdc0b..e81ce43 100644 --- a/physutils/tests/test_tasks.py +++ b/physutils/tests/test_tasks.py @@ -7,10 +7,10 @@ from physutils.tests.utils import create_random_bids_structure -def test_transform_to_physio_phys_file(): - """Test transform_to_physio task.""" +def test_generate_physio_phys_file(): + """Test generate_physio task.""" physio_file = os.path.abspath("physutils/tests/data/ECG.phys") - task = tasks.transform_to_physio(input_file=physio_file, mode="physio") + 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 @@ -23,8 +23,8 @@ def test_transform_to_physio_phys_file(): assert physio_obj.data.shape == (44611,) -def test_transform_to_physio_bids_file(): - """Test transform_to_physio task.""" +def test_generate_physio_bids_file(): + """Test generate_physio task.""" create_random_bids_structure("physutils/tests/data", recording_id="cardiac") bids_parameters = { "subject": "01", @@ -34,7 +34,7 @@ def test_transform_to_physio_bids_file(): "recording": "cardiac", } bids_dir = os.path.abspath("physutils/tests/data/bids-dir") - task = tasks.transform_to_physio( + task = tasks.generate_physio( input_file=bids_dir, mode="bids", bids_parameters=bids_parameters, @@ -53,7 +53,7 @@ def test_transform_to_physio_bids_file(): assert isinstance(physio_obj, physio.Physio) -def test_transform_to_physio_auto(): +def test_generate_physio_auto(): create_random_bids_structure("physutils/tests/data", recording_id="cardiac") bids_parameters = { "subject": "01", @@ -63,7 +63,7 @@ def test_transform_to_physio_auto(): "recording": "cardiac", } bids_dir = os.path.abspath("physutils/tests/data/bids-dir") - task = tasks.transform_to_physio( + task = tasks.generate_physio( input_file=bids_dir, mode="auto", bids_parameters=bids_parameters, @@ -82,9 +82,9 @@ def test_transform_to_physio_auto(): assert isinstance(physio_obj, physio.Physio) -def test_transform_to_physio_auto_error(caplog): +def test_generate_physio_auto_error(caplog): bids_dir = os.path.abspath("physutils/tests/data/non-bids-dir") - task = tasks.transform_to_physio( + task = tasks.generate_physio( input_file=bids_dir, mode="auto", bids_channel="cardiac", From 94b711ccaf2ff5108590ac2dabd7c238c104b478 Mon Sep 17 00:00:00 2001 From: Stefano Moia Date: Wed, 9 Oct 2024 21:23:25 +0200 Subject: [PATCH 14/15] Remove extras --- physutils/io.py | 9 +++++++-- physutils/tasks.py | 1 - setup.cfg | 8 ++++++-- 3 files changed, 13 insertions(+), 5 deletions(-) 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 index 3da6c56..686db19 100644 --- a/physutils/tasks.py +++ b/physutils/tasks.py @@ -11,7 +11,6 @@ # from loguru import logger - try: from pydra import task except ImportError: diff --git a/setup.cfg b/setup.cfg index 86e5486..619fbf5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,8 +24,6 @@ install_requires = matplotlib numpy >=1.9.3 loguru - pydra - 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 From f2a2d923f9532d03742979d2d86d57c0385d63bc Mon Sep 17 00:00:00 2001 From: Marie-Eve Picard Date: Fri, 25 Oct 2024 13:35:48 -0400 Subject: [PATCH 15/15] Fix test tasks --- physutils/tasks.py | 2 +- physutils/tests/test_tasks.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/physutils/tasks.py b/physutils/tasks.py index 686db19..451ac89 100644 --- a/physutils/tasks.py +++ b/physutils/tasks.py @@ -12,7 +12,7 @@ # from loguru import logger try: - from pydra import task + from pydra.mark import task except ImportError: from .utils import task diff --git a/physutils/tests/test_tasks.py b/physutils/tests/test_tasks.py index e81ce43..4c6284e 100644 --- a/physutils/tests/test_tasks.py +++ b/physutils/tests/test_tasks.py @@ -38,14 +38,14 @@ def test_generate_physio_bids_file(): input_file=bids_dir, mode="bids", bids_parameters=bids_parameters, - bids_channel="cardiac", + 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.bids_channel == "cardiac" + assert task.inputs.col_physio_type == "cardiac" task() @@ -67,14 +67,14 @@ def test_generate_physio_auto(): input_file=bids_dir, mode="auto", bids_parameters=bids_parameters, - bids_channel="cardiac", + 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.bids_channel == "cardiac" + assert task.inputs.col_physio_type == "cardiac" task() @@ -87,13 +87,13 @@ def test_generate_physio_auto_error(caplog): task = tasks.generate_physio( input_file=bids_dir, mode="auto", - bids_channel="cardiac", + 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_channel == "cardiac" + assert task.inputs.col_physio_type == "cardiac" try: task()