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

Integrate physutils - Physio object usage #54

Merged
merged 40 commits into from
Aug 25, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
2536086
Add physutils dependency placeholder
maestroque Jun 12, 2024
f047587
Initial Physio object integration in cardiac.py
maestroque Jun 12, 2024
c7fc2eb
Style fixes
maestroque Jun 12, 2024
ec8a3de
Minor cardiac.py physio initialization fix
maestroque Jun 12, 2024
67c4d21
Integrate Physio object for respiratory and multimodal metrics
maestroque Jun 12, 2024
2ffa465
Add sample support for object and function oriented usage of metric f…
maestroque Jun 12, 2024
9fa3467
Make metric functions operations
maestroque Jun 24, 2024
380ab65
Add Physio and non-Physio compatibility to cardiac.py
maestroque Jun 24, 2024
59ee6c8
Add Physio and non-Physio compatibility to chest_belt.py
maestroque Jun 24, 2024
56abb72
Add Physio and non-Physio compatibility to retroicor
maestroque Jun 24, 2024
2c19a65
Fix CRF, iCRF and RRF calculation
maestroque Jun 24, 2024
c28914c
Fix cardiac_phase test, return Physio object and metric to update his…
maestroque Jun 24, 2024
625d45f
Fix chest_belt tests, return Physio object and metric to update histo…
maestroque Jun 24, 2024
acf960d
Update RVT test
maestroque Jun 24, 2024
62e4b6f
Fix respiratory_phase_smoke test
maestroque Jun 24, 2024
e60d49e
Return physio in retroicor and specify raised error type in test_mirr…
maestroque Jun 24, 2024
3dbf656
Add loguru as a dependency
maestroque Jun 24, 2024
f7d7811
Add Physio object use cardiac phase unit test
maestroque Jun 25, 2024
907faba
Remove test log
maestroque Jun 25, 2024
2d9fd75
Add physutils dependency and peakdet version
maestroque Jul 18, 2024
366175a
Update peakdet dependency
maestroque Jul 19, 2024
533c80e
Minor fix
maestroque Jul 22, 2024
3fba78c
circleci: python version upgrade
maestroque Jul 22, 2024
43576d0
circleci: python version migration to 3.8 and 3.12
maestroque Jul 22, 2024
e2f2241
Minor CI fix
maestroque Jul 22, 2024
9efbf5f
CI: Change minimum python version to 3.9
maestroque Jul 22, 2024
ba5dcbc
CI: Test for python 3.9 and 3.11
maestroque Jul 22, 2024
9f5e162
Store computed metrics inside the Physio object
maestroque Jul 22, 2024
696d29c
Merge branch 'physiopy:master' into integrate-physutils
maestroque Jul 23, 2024
daaed9c
Update .all-contributorsrc
maestroque Jul 23, 2024
00b0f56
Merge branch 'physiopy:master' into integrate-physutils
maestroque Aug 9, 2024
bc300f0
Add wrapper to determine whether to return a metric or a physio object
maestroque Aug 10, 2024
9883ce2
[deps] Cap numpy to <2.0
maestroque Aug 10, 2024
181304f
[docs]: Update computing metrics
maestroque Aug 10, 2024
43107db
Metric or Physio returning optimization
maestroque Aug 11, 2024
b0f4110
Add Metric class for easier properties accessing
maestroque Aug 11, 2024
10509bd
More unit tests, physio/metric wrapper optimization, doc fixes
maestroque Aug 21, 2024
612c616
Minor fix
maestroque Aug 21, 2024
e528d21
[test] Fix failing warning assertion
maestroque Aug 25, 2024
6266b6f
Add updated physutils version
maestroque Aug 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
103 changes: 55 additions & 48 deletions phys2denoise/metrics/cardiac.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Denoising metrics for cardio recordings."""
import numpy as np
from physutils import io, physio

from .. import references
from ..due import due
Expand All @@ -8,7 +9,7 @@
from .utils import convolve_and_rescale


def _cardiac_metrics(card, peaks, samplerate, metric, window=6, central_measure="mean"):
def _cardiac_metrics(data, metric, window=6, central_measure="mean"):
"""
Compute cardiac metrics.

Expand All @@ -21,12 +22,8 @@ def _cardiac_metrics(card, peaks, samplerate, metric, window=6, central_measure=

Parameters
----------
card : list or 1D numpy.ndarray
Timeseries of recorded cardiac signal
peaks : list or 1D numpy.ndarray
array of peak indexes for card.
samplerate : float
Sampling rate for card, in Hertz.
data : Physio_like
Object containing the timeseries of the recorded cardiac signal
metrics : "hbi", "hr", "hrv", string
Cardiac metric(s) to calculate.
window : float, optional
Expand Down Expand Up @@ -71,8 +68,11 @@ def _cardiac_metrics(card, peaks, samplerate, metric, window=6, central_measure=
Annual International Conference of the IEEE Engineering in Medicine and
Biology Society (EMBC), doi: 10.1109/EMBC.2016.7591347.
"""
# Initialize physio object
data = physio.check_physio(data, ensure_fs=True, copy=True)

# Convert window to samples, but halves it.
halfwindow_samples = int(round(window * samplerate / 2))
halfwindow_samples = int(round(window * data.fs / 2))

if central_measure in ["mean", "average", "avg"]:
central_measure_operator = np.mean
Expand All @@ -85,14 +85,17 @@ def _cardiac_metrics(card, peaks, samplerate, metric, window=6, central_measure=
f" {central_measure} is not a supported metric of centrality."
)

idx_arr = np.arange(len(card))
idx_arr = np.arange(len(data))
idx_min = afsw(idx_arr, np.min, halfwindow_samples)
idx_max = afsw(idx_arr, np.max, halfwindow_samples)

card_met = np.empty_like(card)
card_met = np.empty_like(data)
for n, i in enumerate(idx_min):
diff = (
np.diff(peaks[np.logical_and(peaks >= i, peaks <= idx_max[n])]) / samplerate
np.diff(
data.peaks[np.logical_and(data.peaks >= i, data.peaks <= idx_max[n])]
)
/ data.fs
)
if metric == "hbi":
card_met[n] = central_measure_operator(diff) if diff.size > 0 else 0
Expand All @@ -109,13 +112,13 @@ def _cardiac_metrics(card, peaks, samplerate, metric, window=6, central_measure=
card_met[np.isnan(card_met)] = 0.0

# Convolve with crf and rescale
card_met = convolve_and_rescale(card_met, crf(samplerate), rescale="rescale")
card_met = convolve_and_rescale(card_met, crf(data.fs), rescale="rescale")

return card_met


@due.dcite(references.CHANG_CUNNINGHAM_GLOVER_2009)
def heart_rate(card, peaks, samplerate, window=6, central_measure="mean"):
def heart_rate(data, window=6, central_measure="mean"):
"""
Compute average heart rate (HR) in a sliding window.

Expand All @@ -125,12 +128,8 @@ def heart_rate(card, peaks, samplerate, window=6, central_measure="mean"):

Parameters
----------
card : list or 1D numpy.ndarray
Timeseries of recorded cardiac signal
peaks : list or 1D numpy.ndarray
array of peak indexes for card.
samplerate : float
Sampling rate for card, in Hertz.
data : Physio_like
Object containing the timeseries of the recorded cardiac signal
window : float, optional
Size of the sliding window, in seconds.
Default is 6.
Expand Down Expand Up @@ -167,13 +166,11 @@ def heart_rate(card, peaks, samplerate, window=6, central_measure="mean"):
Annual International Conference of the IEEE Engineering in Medicine and
Biology Society (EMBC), doi: 10.1109/EMBC.2016.7591347.
"""
return _cardiac_metrics(
card, peaks, samplerate, metric="hrv", window=6, central_measure="mean"
)
return _cardiac_metrics(data, metric="hrv", window=6, central_measure="mean")


@due.dcite(references.PINHERO_ET_AL_2016)
def heart_rate_variability(card, peaks, samplerate, window=6, central_measure="mean"):
def heart_rate_variability(data, window=6, central_measure="mean"):
"""
Compute average heart rate variability (HRV) in a sliding window.

Expand All @@ -183,12 +180,8 @@ def heart_rate_variability(card, peaks, samplerate, window=6, central_measure="m

Parameters
----------
card : list or 1D numpy.ndarray
Timeseries of recorded cardiac signal
peaks : list or 1D numpy.ndarray
array of peak indexes for card.
samplerate : float
Sampling rate for card, in Hertz.
data : Physio_like
Object containing the timeseries of the recorded cardiac signal
window : float, optional
Size of the sliding window, in seconds.
Default is 6.
Expand Down Expand Up @@ -223,24 +216,18 @@ def heart_rate_variability(card, peaks, samplerate, window=6, central_measure="m
Annual International Conference of the IEEE Engineering in Medicine and
Biology Society (EMBC), doi: 10.1109/EMBC.2016.7591347.
"""
return _cardiac_metrics(
card, peaks, samplerate, metric="hrv", window=6, central_measure="std"
)
return _cardiac_metrics(data, metric="hrv", window=6, central_measure="std")


@due.dcite(references.CHEN_2020)
def heart_beat_interval(card, peaks, samplerate, window=6, central_measure="mean"):
def heart_beat_interval(data, window=6, central_measure="mean"):
"""
Compute average heart beat interval (HBI) in a sliding window.

Parameters
----------
card : list or 1D numpy.ndarray
Timeseries of recorded cardiac signal
peaks : list or 1D numpy.ndarray
array of peak indexes for card.
samplerate : float
Sampling rate for card, in Hertz.
data : Physio_like
Object containing the timeseries of the recorded cardiac signal
window : float, optional
Size of the sliding window, in seconds.
Default is 6.
Expand Down Expand Up @@ -272,23 +259,19 @@ def heart_beat_interval(card, peaks, samplerate, window=6, central_measure="mean
.. [1] J. E. Chen et al., "Resting-state "physiological networks"", Neuroimage,
vol. 213, pp. 116707, 2020.
"""
return _cardiac_metrics(
card, peaks, samplerate, metric="hbi", window=6, central_measure="mean"
)
return _cardiac_metrics(data, metric="hbi", window=6, central_measure="mean")


def cardiac_phase(peaks, sample_rate, slice_timings, n_scans, t_r):
def cardiac_phase(data, slice_timings, n_scans, t_r, fs=None, peaks=None):
"""Calculate cardiac phase from cardiac peaks.

Assumes that timing of cardiac events are given in same units
as slice timings, for example seconds.

Parameters
----------
peaks : 1D array_like
Cardiac peak times, in seconds.
sample_rate : float
Sample rate of physio, in Hertz.
data : Physio_like
Object containing the timeseries of the recorded cardiac signal
slice_timings : 1D array_like
Slice times, in seconds.
n_scans : int
Expand All @@ -301,11 +284,35 @@ def cardiac_phase(peaks, sample_rate, slice_timings, n_scans, t_r):
phase_card : array_like
Cardiac phase signal, of shape (n_scans,)
"""
if isinstance(data, physio.Physio):
# Initialize physio object
data = physio.check_physio(data, ensure_fs=True, copy=True)
elif fs is not None and peaks is not None:
data = io.load_physio(data, fs=fs)
data._metadata["peaks"] = peaks
else:
raise ValueError(
"""
To use this function you should either provide a Physio object
with existing peaks metadata (e.g. using the peakdet module), or
by providing the physiological data timeseries, the sampling frequency,
and the peak indices separately.
"""
)
if not data.peaks:
raise ValueError(
"""
Peaks must be a non-empty list.
Make sure to run peak detection on your physiological data first,
using the peakdet module, or other software of your choice.
"""
)

assert slice_timings.ndim == 1, "Slice times must be a 1D array"
n_slices = np.size(slice_timings)
phase_card = np.zeros((n_scans, n_slices))

card_peaks_sec = peaks / sample_rate
card_peaks_sec = data.peaks / data.fs
for i_slice in range(n_slices):
# generate slice acquisition timings across all scans
times_crSlice = t_r * np.arange(n_scans) + slice_timings[i_slice]
Expand Down
Loading