-
Notifications
You must be signed in to change notification settings - Fork 4
Add Physio object generation from BIDS #4
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
Changes from all commits
e6b9673
ecb73be
3440fe8
344ce3d
f6b3d9f
efeaccc
6bafa70
c281917
1b0deaa
0c324c5
2b0deb9
038be86
82d6ee9
bbe49a6
d82ef72
ffa8e71
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -125,3 +125,7 @@ dmypy.json | |
| .pyre/ | ||
|
|
||
| .vscode/ | ||
|
|
||
| # Test Data | ||
| physutils/tests/data/bids-dir | ||
| tmp.* | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -220,10 +220,12 @@ def new_physio_like( | |
|
|
||
| if suppdata is None: | ||
| suppdata = ref_physio._suppdata if copy_suppdata else None | ||
|
|
||
| label = ref_physio.label if copy_label else None | ||
| physio_type = ref_physio.physio_type if copy_physio_type else None | ||
| computed_metrics = list(ref_physio.computed_metrics) if copy_computed_metrics else [] | ||
| computed_metrics = ( | ||
| dict(ref_physio.computed_metrics) if copy_computed_metrics else {} | ||
| ) | ||
|
|
||
| # make new class | ||
| out = ref_physio.__class__( | ||
|
|
@@ -340,7 +342,7 @@ def __init__( | |
| reject=np.empty(0, dtype=int), | ||
| ) | ||
| self._suppdata = None if suppdata is None else np.asarray(suppdata).squeeze() | ||
| self._computed_metrics = [] | ||
| self._computed_metrics = dict() | ||
|
|
||
| def __array__(self): | ||
| return self.data | ||
|
|
@@ -542,3 +544,49 @@ def neurokit2phys( | |
| metadata = dict(peaks=peaks) | ||
|
|
||
| return cls(data, fs=fs, metadata=metadata, **kwargs) | ||
|
|
||
|
|
||
| class MRIConfig: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe I missed it, but is there any test to cover that class ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it is not used yet, because it would need detection of the trigger channel in order to initialize it. I can open an issue defining the next steps for it, and I'll implement it if there is time after the workflow |
||
| """ | ||
| Class to hold MRI configuration information | ||
|
|
||
| Parameters | ||
| ---------- | ||
| slice_timings : 1D array_like | ||
| Slice timings in seconds | ||
| n_scans : int | ||
| Number of volumes in the MRI scan | ||
| tr : float | ||
| Repetition time in seconds | ||
| """ | ||
|
|
||
| def __init__(self, slice_timings=None, n_scans=None, tr=None): | ||
| if np.ndim(slice_timings) > 1: | ||
| raise ValueError("Slice timings must be a 1-dimensional array.") | ||
|
|
||
| self._slice_timings = np.asarray(slice_timings) | ||
| self._n_scans = int(n_scans) | ||
| self._tr = float(tr) | ||
| logger.debug(f"Initializing new MRIConfig object: {self}") | ||
|
|
||
| def __str__(self): | ||
| return "{name}(n_scans={n_scans}, tr={tr})".format( | ||
| name=self.__class__.__name__, | ||
| n_scans=self._n_scans, | ||
| tr=self._tr, | ||
| ) | ||
|
|
||
| @property | ||
| def slice_timings(self): | ||
| """Slice timings in seconds""" | ||
| return self._slice_timings | ||
|
|
||
| @property | ||
| def n_scans(self): | ||
| """Number of volumes in the MRI scan""" | ||
| return self._n_scans | ||
|
|
||
| @property | ||
| def tr(self): | ||
| """Repetition time in seconds""" | ||
| return self._tr | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,11 @@ | |
| import pytest | ||
|
|
||
| from physutils import io, physio | ||
| from physutils.tests.utils import filter_physio, get_test_data_path | ||
| from physutils.tests.utils import ( | ||
| create_random_bids_structure, | ||
| filter_physio, | ||
| get_test_data_path, | ||
| ) | ||
|
|
||
|
|
||
| def test_load_physio(caplog): | ||
|
|
@@ -46,6 +50,43 @@ def test_load_physio(caplog): | |
| io.load_physio([1, 2, 3]) | ||
|
|
||
|
|
||
| def test_load_from_bids(): | ||
| create_random_bids_structure("physutils/tests/data", recording_id="cardiac") | ||
| phys_array = io.load_from_bids( | ||
| "physutils/tests/data/bids-dir", | ||
| subject="01", | ||
| session="01", | ||
| task="rest", | ||
| run="01", | ||
| recording="cardiac", | ||
| ) | ||
|
|
||
| for col in phys_array.keys(): | ||
| assert isinstance(phys_array[col], physio.Physio) | ||
| # The data saved are the ones after t_0 = -3s | ||
| assert phys_array[col].data.size == 80000 | ||
| assert phys_array[col].fs == 10000.0 | ||
| assert phys_array[col].history[0][0] == "physutils.io.load_from_bids" | ||
|
|
||
|
|
||
| def test_load_from_bids_no_rec(): | ||
| create_random_bids_structure("physutils/tests/data") | ||
| phys_array = io.load_from_bids( | ||
| "physutils/tests/data/bids-dir", | ||
| subject="01", | ||
| session="01", | ||
| task="rest", | ||
| run="01", | ||
| ) | ||
|
|
||
| for col in phys_array.keys(): | ||
| assert isinstance(phys_array[col], physio.Physio) | ||
| # The data saved are the ones after t_0 = -3s | ||
| assert phys_array[col].data.size == 80000 | ||
| assert phys_array[col].fs == 10000.0 | ||
| assert phys_array[col].history[0][0] == "physutils.io.load_from_bids" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be better to delete the bids dataset at the end of the function or just to add the |
||
|
|
||
|
|
||
| def test_save_physio(tmpdir): | ||
| pckl = io.load_physio(get_test_data_path("ECG.phys"), allow_pickle=True) | ||
| out = io.save_physio(tmpdir.join("tmp").purebasename, pckl) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,9 +2,12 @@ | |
| Utilities for testing | ||
| """ | ||
|
|
||
| import json | ||
| from os import makedirs | ||
| from os.path import join as pjoin | ||
|
|
||
| import numpy as np | ||
| import pandas as pd | ||
| from pkg_resources import resource_filename | ||
| from scipy import signal | ||
|
|
||
|
|
@@ -77,3 +80,91 @@ def filter_physio(data, cutoffs, method, *, order=3): | |
| filtered = physio.new_physio_like(data, signal.filtfilt(b, a, data)) | ||
|
|
||
| return filtered | ||
|
|
||
|
|
||
| def create_random_bids_structure(data_dir, recording_id=None): | ||
|
|
||
| dataset_description = { | ||
| "Name": "Example BIDS Dataset", | ||
| "BIDSVersion": "1.7.0", | ||
| "License": "", | ||
| "Authors": ["Author1", "Author2"], | ||
| "Acknowledgements": "", | ||
| "HowToAcknowledge": "", | ||
| "Funding": "", | ||
| "ReferencesAndLinks": "", | ||
| "DatasetDOI": "", | ||
| } | ||
|
|
||
| physio_json = { | ||
| "SamplingFrequency": 10000.0, | ||
| "StartTime": -3, | ||
| "Columns": [ | ||
| "time", | ||
| "respiratory_chest", | ||
| "trigger", | ||
| "cardiac", | ||
| "respiratory_CO2", | ||
| "respiratory_O2", | ||
| ], | ||
| } | ||
|
|
||
| # Create BIDS structure directory | ||
| subject_id = "01" | ||
| session_id = "01" | ||
| task_id = "rest" | ||
| run_id = "01" | ||
| recording_id = recording_id | ||
|
|
||
| bids_dir = pjoin( | ||
| data_dir, "bids-dir", f"sub-{subject_id}", f"ses-{session_id}", "func" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should make sure that the test covers the possibility to have the recording entity mentioned in the filename. Ex.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry @maestroque I think my comment was not clear. The tests should cover the possibility to have filenames with the |
||
| ) | ||
| makedirs(bids_dir, exist_ok=True) | ||
|
|
||
| # Create dataset_description.json | ||
| with open(pjoin(data_dir, "bids-dir", "dataset_description.json"), "w") as f: | ||
| json.dump(dataset_description, f, indent=4) | ||
|
|
||
| if recording_id is not None: | ||
| filename_body = f"sub-{subject_id}_ses-{session_id}_task-{task_id}_run-{run_id}_recording-{recording_id}" | ||
| else: | ||
| filename_body = f"sub-{subject_id}_ses-{session_id}_task-{task_id}_run-{run_id}" | ||
|
|
||
| # Create physio.json | ||
| with open( | ||
| pjoin( | ||
| bids_dir, | ||
| f"{filename_body}_physio.json", | ||
| ), | ||
| "w", | ||
| ) as f: | ||
| json.dump(physio_json, f, indent=4) | ||
|
|
||
| # Initialize tsv file with random data columns and a time column | ||
| num_rows = 100000 | ||
| num_cols = 6 | ||
| time_offset = 2 | ||
| time = ( | ||
| np.arange(num_rows) / physio_json["SamplingFrequency"] | ||
| + physio_json["StartTime"] | ||
| - time_offset | ||
| ) | ||
| data = np.column_stack((time, np.random.rand(num_rows, num_cols - 1).round(8))) | ||
| df = pd.DataFrame(data) | ||
|
|
||
| # Compress dataframe into tsv.gz | ||
| tsv_gz_file = pjoin( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I understand why the file is first saved as a tsp, then loaded and saved again as a compressed file. I think you can just directly save
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, you are right, it was left as such from debugging |
||
| bids_dir, | ||
| f"{filename_body}_physio.tsv.gz", | ||
| ) | ||
|
|
||
| df.to_csv( | ||
| tsv_gz_file, | ||
| sep="\t", | ||
| index=False, | ||
| header=False, | ||
| float_format="%.8e", | ||
| compression="gzip", | ||
| ) | ||
|
|
||
| return bids_dir | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to add the parameter
recording=None? The reason is that according to the current version of the BIDS spec physio data with different sampling rate need to be saved under different file names using therecordingentity to differentiate them apart.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And
recordingshould be added when you calllayout.getThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, done!