Skip to content

Commit 92995cd

Browse files
committed
Rewrite retroicor computation consequently, but allow for multiple data types entry and for interactions!
1 parent c66aab3 commit 92995cd

File tree

1 file changed

+189
-110
lines changed

1 file changed

+189
-110
lines changed

phys2denoise/metrics/multimodal.py

Lines changed: 189 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,163 @@
11
"""These functions compute RETROICOR regressors (Glover et al. 2000)."""
2+
from copy import deepcopy as dc
23

34
import numpy as np
5+
from peakdet import Physio as pkphysio
46
from physutils import io, physio
57

68
from .. import references
79
from ..due import due
8-
from .cardiac import cardiac_phase
9-
from .chest_belt import respiratory_phase
10-
from .utils import return_physio_or_metric
10+
from .phase import cardiac, respiratory
1111

1212

1313
@due.dcite(references.GLOVER_2000)
14-
@return_physio_or_metric()
15-
@physio.make_operation()
1614
def retroicor(
17-
data,
18-
t_r,
19-
n_scans,
2015
slice_timings,
21-
n_harmonics,
22-
physio_type=None,
23-
fs=None,
24-
cardiac_peaks=None,
25-
**kwargs,
16+
resp_data=None,
17+
card_data=None,
18+
resp_fs=None,
19+
card_fs=None,
20+
n_harmonics=2,
21+
base_offset=0,
22+
nbins="p2d",
23+
compute_interaction=True,
2624
):
2725
"""Compute RETROICOR regressors.
2826
2927
Parameters
3028
----------
31-
data : physutils.Physio, np.ndarray, or array-like object
32-
Object containing the timeseries of the recorded respiratory or cardiac signal
33-
t_r : float
34-
Imaging sample rate, in seconds.
35-
n_scans : int
36-
Number of volumes in the imaging run.
37-
slice_timings : array_like
29+
slice_timings : float or array_like
3830
Slice times, in seconds.
39-
n_harmonics : int
40-
???
41-
card : bool, optional
42-
Whether the physio data correspond to cardiac or repiratory signal.
43-
resp : bool, optional
44-
Whether the physio data correspond to cardiac or repiratory signal.
45-
46-
Returns
47-
-------
48-
retroicor_regressors : list
49-
phase : array_like
50-
2D array of shape (n_scans, n_slices)
31+
resp_data : None, physutils.Physio, or array-like object, optional
32+
Respiratory data, either a physio object or an array-like object describing the
33+
recorded respiratory changes *timeseries*
34+
card_data : None, physutils.Physio, or array-like object, optional
35+
Cardiac data, either a physio object or an array-like object describing the
36+
recorded cardiac *peaks' indices* (!!!)
37+
resp_fs : None or int, optional
38+
Respiratory data sampling rate, optional if resp_data is a physio object,
39+
necessary otherwise.
40+
card_fs : None or int, optional
41+
Cardiac data sampling rate, optional if card_data is a physio object,
42+
necessary otherwise.
43+
n_harmonics : int > 0, optional
44+
Number of harmonics to compute (for all modalities). Must be > 0. Default is 2.
45+
base_offset : int or float >=0, optional
46+
Offset time for the whole neuroimaging data wrt physiological data, default is 0.
47+
Must be >=0. Negative offsets are currently not supported.
48+
Note that neuroimaging data offset is normally found in BIDS as a metadata of the
49+
physiological recording, so the signal must be inverted compared to that field.
50+
nbins : int or string, optional
51+
Number of bins to consider in making the histogram or method to estimate such
52+
number when computing respiratory phase. The default option is "p2d" and
53+
corresponds to either a quarter of the data amount or 100, whatever is higher.
54+
Any option supported by `np.histogram` is also supported here.
55+
compute_interaction : bool, optional
56+
If respiratory and cardiac data are both given, compute their interactive terms,
57+
see [2]
58+
Default is True.
5159
5260
Notes
5361
-----
5462
RETROICOR regressors should be regressed from the imaging data *before*
55-
any other preprocessing, including slice-timing correction and motion correction.
63+
any other preprocessing, including slice-timing correction and motion correction,
64+
and it should be voxel or slice specific.
65+
Despite this, many people regress one single set of regressors in the GLM - in that
66+
case, don't use any offset.
67+
68+
This function does not work like the others due to the issue of having multiple
69+
possible physio objects in entrance. It will return all physio objects with all
70+
computed retroicor regressors, so pay attention to repetitions!
71+
72+
Does not support voxelwise correction yet, thus coefficients are not given.
5673
5774
References
5875
----------
5976
.. [1] G. H. Glover & T. Q. L. Ress, “Image_based method for retrospective
6077
correction of physiological motion effects in fMRI: RETROICOR“, Magn. Reson. Med.,
6178
issue 1, vol. 44, pp. 162-167, 2000.
79+
.. [2] A.K. Harvey & all, "Brainstem functional magnetic resonance imaging:
80+
Disentangling signal from physiological noise", JMRI, issue 6, vol. 28, 2008.
81+
82+
Returns
83+
-------
84+
retroicor_regressors : dict
85+
Retroicor regressors are organised in a dictionary of two dictionaries, one for
86+
retroicor regressors and one for phases. These dictionaries have one entry per
87+
slice.
88+
All entries are lists, always organised: cardiac regressors first, then
89+
respiratory, then interactions.
90+
91+
If no physutils.Physio object is given, they will be returned as they are.
92+
resp_data : physutils.Physio object
93+
If resp_data is given as physutils.Physio object, returns it with the computed
94+
retroicor regressors in its metrics metadata.
95+
card_data : physutils.Physio object
96+
If card_data is given as physutils.Physio object, returns it with the computed
97+
retroicor regressors in its metrics metadata.
6298
"""
63-
if isinstance(data, physio.Physio):
64-
# Initialize physio object
65-
data = physio.check_physio(data, ensure_fs=True, copy=True)
66-
if data.physio_type is None and physio_type is not None:
67-
data._physio_type = physio_type
68-
elif data.physio_type is None and physio_type is None:
99+
if n_harmonics <= 0:
100+
raise ValueError(
101+
f"The number of harmonics must be > 0 but {n_harmonics} was given."
102+
)
103+
104+
# Parse input data
105+
return_physio = False
106+
107+
if resp_data is not None:
108+
if isinstance(resp_data, physio.Physio):
109+
# Initialize physio object
110+
resp_data = physio.check_physio(resp_data, ensure_fs=True, copy=True)
111+
return_physio = True
112+
elif isinstance(resp_data, pkphysio):
113+
# Retrocompatibility with peakdet
114+
resp_data = io.load_physio(resp_data.data, fs=resp_data.fs)
115+
elif resp_fs is not None:
116+
resp_data = physio.Physio(resp_data, fs=resp_fs)
117+
else:
69118
raise ValueError(
70119
"""
71-
Since the provided Physio object does not specify a `physio_type`,
72-
this function's `physio_type` parameter must be specified as a
73-
value from {'cardiac', 'respiratory'}
120+
resp_data is not a Physio object but resp_fs was not specified.
121+
To use this function with respiratory data you should either provide a
122+
Physio object describing the physiological data timeseries, or an
123+
array-like object *and* the sampling frequency.
124+
"""
125+
)
126+
elif card_data is None:
127+
return None
128+
129+
if card_data is not None:
130+
if isinstance(card_data, physio.Physio):
131+
# Initialize physio object
132+
card_data = physio.check_physio(card_data, ensure_fs=True, copy=True)
133+
return_physio = True
134+
elif isinstance(card_data, pkphysio):
135+
# Retrocompatibility with peakdet
136+
peaks = dc(card_data.peaks)
137+
card_data = io.load_physio(card_data.data, fs=card_data.fs)
138+
card_data._metadata["peaks"] = peaks
139+
elif card_fs is not None:
140+
peaks = dc(card_data)
141+
card_data = physio.Physio(np.zeros((card_data[-1] + 1)), fs=card_fs)
142+
card_data._metadata["peaks"] = peaks
143+
else:
144+
raise ValueError(
145+
"""
146+
card_data is not a Physio object but card_fs was not specified.
147+
To use this function with cardiac data you should either provide a
148+
Physio object with cardiac data timeseries and peaks, or an
149+
array-like object describing peaks *and* the sampling frequency.
74150
"""
75151
)
76152

77-
elif fs is not None and physio_type is not None:
78-
data = io.load_physio(data, fs=fs)
79-
data._physio_type = physio_type
80-
if data.physio_type == "cardiac":
81-
data._metadata["peaks"] = cardiac_peaks
82-
else:
83-
raise ValueError(
84-
"""
85-
To use this function you should either provide a Physio object
86-
with existing peaks metadata if it describes a cardiac signal
87-
(e.g. using the peakdet module), or
88-
by providing the physiological data timeseries, the sampling frequency,
89-
the physio_type and the peak indices separately.
90-
"""
91-
)
92-
if (
93-
not (hasattr(data, "peaks") and np.any(data.peaks))
94-
and data.physio_type == "cardiac"
95-
):
96-
raise ValueError(
97-
"""
98-
Peaks must be a non-empty list for cardiac data.
99-
Make sure to run peak detection on your cardiac data first,
100-
using the peakdet module, or other software of your choice.
101-
"""
102-
)
153+
if not (hasattr(card_data, "peaks") and np.any(card_data.peaks)):
154+
raise ValueError(
155+
"""
156+
Peaks must be a non-empty list for cardiac data.
157+
Make sure to run peak detection on your cardiac data first,
158+
using the peakdet module, or other software of your choice.
159+
"""
160+
)
103161

104162
slice_timings = np.asarray(slice_timings)
105163
if slice_timings.squeeze().ndim > 1:
@@ -108,48 +166,69 @@ def retroicor(
108166
"provide a single 1D array-like data."
109167
)
110168

111-
n_slices = slice_timings.size
112-
113-
# initialize output variables
114-
retroicor_regressors = []
115-
phase = np.empty((n_scans, n_slices))
116-
117-
for i_slice in range(n_slices):
118-
retroicor_regressors.append(np.empty((n_scans, 2 * n_harmonics)))
119-
120-
# Initialize slice timings for current slice
121-
crslice_timings = t_r * np.arange(n_scans) + slice_timings[i_slice]
169+
# Initialize output variable
170+
retroicor_regressors = {}
171+
phases = {}
122172

173+
# Compute slice-dependent retroicor regressors.
174+
for n, slice_time in enumerate(slice_timings):
175+
phases[n] = {}
176+
offset = base_offset + slice_time
123177
# Compute physiological phases using the timings of physio events (e.g. peaks)
124178
# slice sampling times
125-
if data.physio_type == "cardiac":
126-
phase[:, i_slice] = cardiac_phase(
127-
data,
128-
crslice_timings,
129-
n_scans,
130-
t_r,
131-
)
132-
133-
if data.physio_type == "respiratory":
134-
phase[:, i_slice] = respiratory_phase(
135-
data,
136-
n_scans,
137-
crslice_timings,
138-
t_r,
139-
)
140-
141-
# Compute retroicor regressors
142-
for j_harm in range(n_harmonics):
143-
retroicor_regressors[i_slice][:, 2 * j_harm] = np.cos(
144-
(j_harm + 1) * phase[i_slice]
145-
)
146-
retroicor_regressors[i_slice][:, 2 * j_harm + 1] = np.sin(
147-
(j_harm + 1) * phase[i_slice]
148-
)
149-
150-
data._computed_metrics["retroicor_regressors"] = dict(
151-
metrics=retroicor_regressors, phase=phase
152-
)
153-
retroicor_regressors = dict(metrics=retroicor_regressors, phase=phase)
154-
155-
return data, retroicor_regressors
179+
if card_data is not None:
180+
phases[n]["card"] = cardiac(card_data.peaks, card_data.fs, offset)
181+
182+
if resp_data is not None:
183+
phases[n]["resp"] = respiratory(resp_data.data, resp_data.fs, offset, nbins)
184+
185+
if card_data is not None:
186+
# Cut to same length. It'll be better later.
187+
length = np.min(phases[n]["card"].size, phases[n]["resp"].size)
188+
phases[n]["card"] = phases[n]["card"][:length]
189+
phases[n]["resp"] = phases[n]["resp"][:length]
190+
191+
retroicor_regressors[n] = []
192+
193+
# It's not computationally efficient to loop multiple times per if statement,
194+
# but organising regressors this way is better for users.
195+
if card_data is not None:
196+
for m in n_harmonics:
197+
retroicor_regressors[n] = retroicor_regressors[n] + [
198+
np.cos(m * phases[n]["card"]),
199+
np.sin(m * phases[n]["card"]),
200+
]
201+
202+
if resp_data is not None:
203+
for m in n_harmonics:
204+
retroicor_regressors[n] = retroicor_regressors[n] + [
205+
np.cos(m * phases[n]["resp"]),
206+
np.sin(m * phases[n]["resp"]),
207+
]
208+
209+
if compute_interaction and card_data is not None:
210+
for m in n_harmonics:
211+
retroicor_regressors[n] = retroicor_regressors[n] + [
212+
np.cos(m * phases[n]["card"]) * np.cos(m * phases[n]["resp"]),
213+
np.cos(m * phases[n]["card"]) * np.sin(m * phases[n]["resp"]),
214+
np.sin(m * phases[n]["card"]) * np.cos(m * phases[n]["resp"]),
215+
np.sin(m * phases[n]["card"]) * np.sin(m * phases[n]["resp"]),
216+
]
217+
218+
retroicor_regressors = {"retroicor": retroicor_regressors, "phases": phases}
219+
220+
if return_physio:
221+
if card_data is not None:
222+
card_data._computed_metrics["retroicor_regressors"] = retroicor_regressors
223+
224+
if resp_data is not None:
225+
resp_data._computed_metrics["retroicor_regressors"] = retroicor_regressors
226+
227+
if card_data is not None:
228+
return resp_data, card_data
229+
else:
230+
return resp_data
231+
else:
232+
return card_data
233+
else:
234+
return retroicor_regressors

0 commit comments

Comments
 (0)