Skip to content

Commit c66aab3

Browse files
committed
Complete overhaul of phase computation, now at the frequency of the physiological data (like most other metrics) and vectorised for efficiency.
1 parent 6e0bd40 commit c66aab3

File tree

1 file changed

+178
-0
lines changed

1 file changed

+178
-0
lines changed

phys2denoise/metrics/phase.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
"""Denoising metrics for cardio recordings."""
2+
import numpy as np
3+
from loguru import logger
4+
from scipy.interpolation import interp1d
5+
6+
7+
def cardiac(peaks, fs, offset=0):
8+
"""Calculate cardiac phase from cardiac peaks.
9+
10+
Assumes that timing of cardiac events are given in same units
11+
as slice timings, for example seconds.
12+
13+
This function will always return just the metric, never a physio object with the
14+
metric inside.
15+
16+
Parameters
17+
----------
18+
peaks : 1D array_like
19+
Indices of peaks in a cardiac array
20+
fs : float
21+
Sampling rate of physiological data in Hz
22+
offset : int or float >=0, optional
23+
Offset time for the neuroimaging data. This includes slice timing (normally
24+
positive) and neuroimaging data offset wrt physiological data (normally positive).
25+
Must be >=0. Negative offsets are currently not supported.
26+
Note that neuroimaging data offset is normally found in BIDS as a metadata of the
27+
physiological recording, so the signal must be inverted compared to that field.
28+
29+
Returns
30+
-------
31+
phase_card : array_like
32+
Cardiac phase signal, sampled at the physiological signal sampling rate and of
33+
length = peaks[-1]
34+
35+
Note
36+
----
37+
This function will always return just the metric, never a physio object with the
38+
metric inside, unlike every other metric function in this module.
39+
40+
This function won't fail, but you will not be able to export regressors, if the
41+
physiological data collected ends before the neuroimaging data.
42+
43+
`time1` and `time2` refer to the original formula from [1]:
44+
phi(t) = 2*pi*(t-t1)/(t2-t1)
45+
They represent the beat right before the reference timepoint and the one after that respectively.
46+
47+
Raises
48+
------
49+
ValueError
50+
If the offset is negative.
51+
52+
References
53+
----------
54+
.. [1] G. H. Glover & T. Q. L. Ress, “Image_based method for retrospective
55+
correction of physiological motion effects in fMRI: RETROICOR“, Magn. Reson. Med.,
56+
issue 1, vol. 44, pp. 162-167, 2000.
57+
"""
58+
if offset < 0:
59+
raise ValueError("Negative offsets are not supported yet.")
60+
61+
# Add a beat before and after the current beats
62+
avg_peak_dist = int(np.diff(peaks).mean().round())
63+
peaks = np.append(
64+
np.append(peaks[0] - avg_peak_dist, peaks),
65+
[peaks[-1] + avg_peak_dist, peaks[-1] + 2 * avg_peak_dist],
66+
)
67+
68+
# Pre-append additional peaks until we get to <0
69+
while peaks[0] >= 0:
70+
peaks = np.append((peaks[0] - avg_peak_dist), peaks)
71+
72+
# Transform in seconds and offset
73+
peaks_sec = peaks / fs - offset
74+
75+
# Create timeline reference for neuroimaging on the peaks time, adding offset,
76+
# accounting for the two extra peaks added above.
77+
time = np.arange(peaks[-3] + 1) / fs
78+
79+
time1 = interp1d(peaks_sec, peaks_sec, kind="previous", assume_sorted=True)(time)
80+
time2 = interp1d(peaks_sec, peaks_sec, kind="next", assume_sorted=True)(time)
81+
82+
return 2 * np.pi * ((time[1:] - time1[:-1]) / (time2[1:] - time1[:-1]))
83+
84+
85+
def respiratory(data, fs, offset=0, nbins="p2d"):
86+
"""Calculate respiratory phase from respiratory signal.
87+
88+
Parameters
89+
----------
90+
data : array-like object
91+
Recorded respiratory signal's timeseries
92+
fs : float
93+
Sampling rate of physiological data in Hz
94+
offset : int or float >=0, optional
95+
Offset time for the neuroimaging data, default is 0. This includes slice timing
96+
(normally positive) and neuroimaging data offset wrt physiological data
97+
(normally positive).
98+
Must be >=0. Negative offsets are currently not supported.
99+
Note that neuroimaging data offset is normally found in BIDS as a metadata of the
100+
physiological recording, so the signal must be inverted compared to that field.
101+
nbins : int or string, optional
102+
Number of bins to consider in making the histogram or method to estimate such
103+
number. The default option is "p2d" and corresponds to either a quarter of the
104+
data amount or 100, whatever is higher.
105+
Any option supported by `np.histogram` is also supported here.
106+
107+
Returns
108+
-------
109+
phase_resp : array_like
110+
Respiratory phase signal, sampled at the physiological signal sampling rate and
111+
of length = data.size
112+
113+
Note
114+
----
115+
This function will always return just the metric, never a physio object with the
116+
metric inside, unlike every other metric function in this module.
117+
118+
This function won't fail, but you will not be able to export regressors, if the
119+
physiological data collected ends before the neuroimaging data.
120+
121+
This is an adapted and vectorized version of the original formula from [1].
122+
The original formula has basically three elements for each timepoint:
123+
- pi
124+
- the area under the curve (AUC) left of any bin that timepoint is in,
125+
divided by the total AUC
126+
- the sign of the (discrete) derivative of the signal at that timepoint
127+
128+
Here it is implemented as:
129+
- pi (duh)
130+
- the cumulative sum of the counts up to the bin of the timepoint, in a histogram
131+
normalised by the amount of data entries (so that total AUC, i.e. cumsum, = 1)
132+
- the sign of the one-hop difference of the signal.
133+
134+
It is, however, exactly the same thing (beside decimal point error).
135+
136+
Also, while the histogram is created on the original data, the search happens with
137+
the offsetted data (if offset is specified). This is so that all slice time shifts
138+
refer to the same histogram.
139+
140+
Raises
141+
------
142+
ValueError
143+
If offset is < 0
144+
145+
References
146+
----------
147+
.. [1] G. H. Glover & T. Q. L. Ress, “Image_based method for retrospective
148+
correction of physiological motion effects in fMRI: RETROICOR“, Magn. Reson. Med.,
149+
issue 1, vol. 44, pp. 162-167, 2000.
150+
"""
151+
if offset < 0:
152+
raise ValueError("Negative offsets are not supported yet.")
153+
elif offset > 0:
154+
# Interpolate data in neuroimaging's offsetted time.
155+
time = np.arange(data.size)
156+
data_offsetted = interp1d(
157+
time, data, kind="linear", fill_value="extrapolate", assume_sorted=True
158+
)(time + offset)
159+
else:
160+
# Skip interpolation
161+
data_offsetted = data
162+
163+
nbins = max(100, int(data.size / 4)) if nbins == "p2d" else nbins
164+
counts, binedge = np.histogram(data, bins=nbins)
165+
# Normalize counts by total to avoid denominator computation later
166+
counts = counts / counts.sum()
167+
# For each element, find its bin
168+
bincenter = (binedge[:-1] + binedge[1:]) / 2
169+
idx = np.searchsorted(bincenter, data_offsetted, side="left")
170+
# Cumulative sum computes AUC left of any bin
171+
cumsum = np.cumsum(counts)
172+
# use the bin index to find the AUC left of it.
173+
raw_phase = np.where(idx > 0, cumsum[idx - 1], 0)
174+
175+
sign = np.sign(np.diff(data))
176+
sign = np.append(sign, sign[-1])
177+
178+
return np.pi * raw_phase * sign

0 commit comments

Comments
 (0)