-
Notifications
You must be signed in to change notification settings - Fork 92
/
combine.py
231 lines (196 loc) · 7.77 KB
/
combine.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
"""
Functions to optimally combine data across echoes.
"""
import logging
import numpy as np
LGR = logging.getLogger("GENERAL")
RepLGR = logging.getLogger("REPORT")
def _combine_t2s(data, tes, ft2s, report=True):
"""
Combine data across echoes using weighted averaging according to voxel-
(and sometimes volume-) wise estimates of T2*.
This method was proposed in :footcite:t:`posse1999enhancement`.
Parameters
----------
data : (M x E x T) array_like
Masked data.
tes : (1 x E) array_like
Echo times in milliseconds.
ft2s : (M [x T] X 1) array_like
Either voxel-wise or voxel- and volume-wise estimates of T2*.
report : bool, optional
Whether to log a description of this step or not. Default is True.
Returns
-------
combined : (M x T) :obj:`numpy.ndarray`
Data combined across echoes according to T2* estimates.
References
----------
.. footbibliography::
"""
if report:
RepLGR.info(
"Multi-echo data were then optimally combined using the "
"T2* combination method \\citep{posse1999enhancement}."
)
n_vols = data.shape[-1]
alpha = tes * np.exp(-tes / ft2s)
if alpha.ndim == 2:
# Voxel-wise T2 estimates
alpha = np.tile(alpha[:, :, np.newaxis], (1, 1, n_vols))
elif alpha.ndim == 3:
# Voxel- and volume-wise T2 estimates
# alpha is currently (S, T, E) but should be (S, E, T) like mdata
alpha = np.swapaxes(alpha, 1, 2)
# If all values across echos are 0, set to 1 to avoid
# divide-by-zero errors
ax0_idx, ax2_idx = np.where(np.all(alpha == 0, axis=1))
alpha[ax0_idx, :, ax2_idx] = 1.0
combined = np.average(data, axis=1, weights=alpha)
return combined
def _combine_paid(data, tes, report=True):
"""
Combine data across echoes using SNR/signal and TE via the
parallel-acquired inhomogeneity desensitized (PAID) ME-fMRI combination
method.
This method was first proposed in :footcite:t:`poser2006bold`.
Parameters
----------
data : (M x E x T) array_like
Masked data.
tes : (1 x E) array_like
Echo times in milliseconds.
report : bool, optional
Whether to log a description of this step or not. Default is True.
Returns
-------
combined : (M x T) :obj:`numpy.ndarray`
Data combined across echoes according to SNR/signal.
References
----------
.. footbibliography::
"""
if report:
RepLGR.info(
"Multi-echo data were then optimally combined using the "
"parallel-acquired inhomogeneity desensitized (PAID) "
"combination method \\citep{poser2006bold}."
)
n_vols = data.shape[-1]
snr = data.mean(axis=-1) / data.std(axis=-1)
alpha = snr * tes
alpha = np.tile(alpha[:, :, np.newaxis], (1, 1, n_vols))
combined = np.average(data, axis=1, weights=alpha)
return combined
def make_optcom(data, tes, adaptive_mask, t2s=None, combmode="t2s", verbose=True):
"""
Optimally combine BOLD data across TEs, using only those echos with reliable signal
across at least three echos. If the number of echos providing reliable signal is greater
than three but less than the total number of collected echos, we assume that later
echos do not provided meaningful signal.
Parameters
----------
data : (S x E x T) :obj:`numpy.ndarray`
Concatenated BOLD data.
tes : (E,) :obj:`numpy.ndarray`
Array of TEs, in seconds.
adaptive_mask : (S,) :obj:`numpy.ndarray`
Array where each value indicates the number of echoes with good signal
for that voxel. This mask may be thresholded; for example, with values
less than 3 set to 0.
For more information on thresholding, see `make_adaptive_mask`.
t2s : (S [x T]) :obj:`numpy.ndarray` or None, optional
Estimated T2* values. Only required if combmode = 't2s'.
Default is None.
combmode : {'t2s', 'paid'}, optional
How to combine data. Either 'paid' or 't2s'. If 'paid', argument 't2s'
is not required. Default is 't2s'.
verbose : :obj:`bool`, optional
Whether to print status updates. Default is True.
Returns
-------
combined : (S x T) :obj:`numpy.ndarray`
Optimally combined data.
Notes
-----
This function supports both the ``'t2s'`` method :footcite:p:`posse1999enhancement`
and the ``'paid'`` method :footcite:p:`poser2006bold`.
The ``'t2s'`` method operates according to the following logic:
1. Estimate voxel- and TE-specific weights based on estimated :math:`T_2^*`:
.. math::
w(T_2^*)_n = \\frac{TE_n * exp(\\frac{-TE}\
{T_{2(est)}^*})}{\\sum TE_n * exp(\\frac{-TE}{T_{2(est)}^*})}
2. Perform weighted average per voxel and TR across TEs based on weights
estimated in the previous step.
References
----------
.. footbibliography::
See Also
--------
:func:`tedana.utils.make_adaptive_mask` : The function used to create the ``adaptive_mask``
parameter.
"""
if data.ndim != 3:
raise ValueError("Input data must be 3D (S x E x T)")
if len(tes) != data.shape[1]:
raise ValueError(
"Number of echos provided does not match second "
"dimension of input data: {0} != "
"{1}".format(len(tes), data.shape[1])
)
if adaptive_mask.ndim != 1:
raise ValueError("Mask is not 1D")
elif adaptive_mask.shape[0] != data.shape[0]:
raise ValueError(
"Mask and data do not have same number of "
"voxels/samples: {0} != {1}".format(adaptive_mask.shape[0], data.shape[0])
)
if combmode not in ["t2s", "paid"]:
raise ValueError("Argument 'combmode' must be either 't2s' or 'paid'")
elif combmode == "t2s" and t2s is None:
raise ValueError("Argument 't2s' must be supplied if 'combmode' is set to 't2s'.")
elif combmode == "paid" and t2s is not None:
LGR.warning(
"Argument 't2s' is not required if 'combmode' is 'paid'. "
"'t2s' array will not be used."
)
if combmode == "paid":
LGR.info(
"Optimally combining data with parallel-acquired "
"inhomogeneity desensitized (PAID) method"
)
else:
if t2s.ndim == 1:
msg = "Optimally combining data with voxel-wise T2* estimates"
else:
msg = "Optimally combining data with voxel- and volume-wise T2* estimates"
LGR.info(msg)
echos_to_run = np.unique(adaptive_mask)
# When there is one good echo, use two
if 1 in echos_to_run:
echos_to_run = np.sort(np.unique(np.append(echos_to_run, 2)))
echos_to_run = echos_to_run[echos_to_run >= 2]
tes = np.array(tes)[np.newaxis, ...] # (1 x E) array_like
combined = np.zeros((data.shape[0], data.shape[2]))
report = True
for i_echo, echo_num in enumerate(echos_to_run):
if echo_num == 2:
# Use the first two echoes for cases where there are
# either one or two good echoes
voxel_idx = np.where(np.logical_and(adaptive_mask > 0, adaptive_mask <= echo_num))[0]
else:
voxel_idx = np.where(adaptive_mask == echo_num)[0]
if combmode == "paid":
combined[voxel_idx, :] = _combine_paid(
data[voxel_idx, :echo_num, :], tes[:, :echo_num]
)
else:
t2s_ = t2s[..., np.newaxis] # add singleton
combined[voxel_idx, :] = _combine_t2s(
data[voxel_idx, :echo_num, :],
tes[:, :echo_num],
t2s_[voxel_idx, ...],
report=report,
)
report = False
return combined