11"""These functions compute RETROICOR regressors (Glover et al. 2000)."""
2+ from copy import deepcopy as dc
23
34import numpy as np
5+ from peakdet import Physio as pkphysio
46from physutils import io , physio
57
68from .. import references
79from ..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 ()
1614def 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