/
estimate.m
310 lines (266 loc) · 9.83 KB
/
estimate.m
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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
function [ obj ] = estimate( obj, varargin )
% Estimate parameters of the HUGE model.
%
% INPUTS:
% obj - A tapas_Huge object containing fMRI time series.
%
% OPTIONAL INPUTS:
% This function accepts optional name-value pair arguments. For a list of
% valid name-value pairs, see the user manual or type 'help
% tapas_huge_property_names'.
%
% OUTPUTS:
% obj - A tapas_Huge object containing the estimation result in the
% 'posterior' property.
%
% EXAMPLES:
% [obj] = ESTIMATE(obj) Invert the HUGE model stored in obj.
%
% [obj] = ESTIMATE(obj, 'K', 2) Set the number of clusters to 2 and
% invert the HUGE model stored in obj.
%
% [obj] = ESTIMATE(obj, 'Verbose', true) Print progress of estimation
% to command line.
%
% [obj] = ESTIMATE(obj, 'Dcm', dcms, 'OmitFromClustering', 1) Import
% data stored in 'dcms' and omit self-connections from clustering.
%
% See also TAPAS_HUGE_DEMO
%
% Author: Yu Yao (yao@biomed.ee.ethz.ch)
% Copyright (C) 2019 Translational Neuromodeling Unit
% Institute for Biomedical Engineering,
% University of Zurich and ETH Zurich.
%
% This file is part of TAPAS, which is released under the terms of the GNU
% General Public Licence (GPL), version 3. For further details, see
% <https://www.gnu.org/licenses/>.
%
% This software is provided "as is", without warranty of any kind, express
% or implied, including, but not limited to the warranties of
% merchantability, fitness for a particular purpose and non-infringement.
%
% This software is intended for research only. Do not use for clinical
% purpose. Please note that this toolbox is under active development.
% Considerable changes may occur in future releases. For support please
% refer to:
% https://github.com/translationalneuromodeling/tapas/issues
%
%% process and check input
obj = obj.optional_inputs(varargin{:});
assert(obj.N > 0, 'TAPAS:HUGE:NoData', 'Cannot estimate without data.')
tapas_huge_compile( );
% save random number generator seed
seed = rng();
% check if filename for storing result is valid
filename = obj.options.nvp.saveto;
if ~isempty(filename)
[pathStr, fileStr, extStr] = fileparts(filename);
if ~strcmpi(extStr,'.mat')
filename = [filename '.mat'];
[pathStr, fileStr, extStr] = fileparts(filename);
end
if isempty(fileStr)
fileStr = ['huge' datestr(now,'-yyyymmdd-HHMMSS')];
end
filename = fullfile(pathStr, [fileStr, extStr]);
save(filename, obj);
end
% decide how to treat confounds
switch lower(obj.options.nvp.confoundsvariant)
case 'default'
obj.options.confVar = double(obj.M > 0);
case 'none'
obj.options.confVar = 0;
case 'global'
obj.options.confVar = 1;
case 'cluster'
obj.options.confVar = 2;
end
if obj.options.confVar && ~obj.M
obj.options.confVar = 0;
warning('TAPAS:HUGE:MissingConfounds',...
['HUGE is configured to use group-level confounds, but no values ' ...
'for confounds were supplied. Proceeding without confounds.']);
end
if obj.K > obj.N
warning('TAPAS:HUGE:LargeK',...
'Number of clusters K exceeds number of subjects N.');
end
% check priors
obj.prior = build_prior(obj);
% starting values
obj = prepare_starting_values(obj);
%% model inversion
obj.aux = struct();
fprintf('Inverting HUGE with %u cluster', obj.K);
if obj.K > 1
fprintf('s');
end
fprintf(' using %s.\n', obj.options.nvp.method);
switch obj.options.nvp.method
case 'VB'
obj = obj.vb_invert( );
case 'MH'
obj = obj.mh_invert( );
end
%% post processing
obj.aux = [];
obj.options.start = rmfield(obj.options.start, {'subjects', 'clusters'});
% if working with simulated data, calculate balanced purity
obj.posterior.bPurity = [];
if ~isempty(obj.model)
model = obj.model;
[~, labels] = max(model.d, [], 2);
obj.posterior.bPurity = tapas_huge_bpurity(labels, obj.posterior.q_nk);
end
% save additional information
obj.posterior.method = obj.options.nvp.method;
obj.posterior.version = obj.version;
obj.posterior.seed = seed;
% save result
if ~isempty(filename)
save(filename, obj);
fprintf('Saved result to "%s".\n', filename);
end
end
function [ prior ] = build_prior( obj )
prior = struct();
% alpha_0
prior.alpha_0 = obj.options.prior.alpha_0;
if isscalar(prior.alpha_0)
prior.alpha_0 = repmat(prior.alpha_0, obj.K, 1);
end
assert(size(prior.alpha_0, 1) == obj.K && size(prior.alpha_0, 2) == 1 && ...
all(prior.alpha_0 > 0), 'TAPAS:HUGE:Prior', ...
'Prior alpha0 must be positive column vector of length %u.', obj.K);
% S_0
prior.S_0 = obj.options.nvp.priorclustervariance;
if isscalar(prior.S_0)
prior.S_0 = eye(obj.idx.P_c)*prior.S_0;
end
assert(size(prior.S_0, 1) == obj.idx.P_c, 'TAPAS:HUGE:Prior',...
'PriorClusterVariance must be of size %ux%u.', obj.idx.P_c, obj.idx.P_c);
% m_0
prior.m_0 = obj.options.nvp.priorclustermean;
if isscalar(prior.m_0)
prior.m_0 = repmat(prior.m_0, 1, obj.idx.P_c);
end
assert(numel(prior.m_0) == obj.idx.P_c, 'TAPAS:HUGE:Prior', ...
'PriorClusterMean must be row vector of length %u.', obj.idx.P_c);
prior.m_0 = prior.m_0(:)';
% nu_0
prior.nu_0 = obj.options.nvp.priordegree;
assert(all(prior.nu_0(:) > 0), 'TAPAS:HUGE:Prior', ...
'PriorDegree must be positive scalar.');
% tau_0
prior.tau_0 = obj.options.nvp.priorvarianceratio;
assert(all(prior.tau_0(:) > 0), 'TAPAS:HUGE:Prior', ...
'PriorCovarianceRatio must be positive scalar.');
% mu_h
prior.mu_h = obj.options.prior.mu_h;
if isscalar(prior.mu_h)
prior.mu_h = repmat(prior.mu_h, 1, obj.idx.P_h);
end
assert(size(prior.mu_h, 2) == obj.idx.P_h && ...
size(prior.mu_h, 1) == 1, 'TAPAS:HUGE:Prior', ...
'Prior homogenous mean must be row vector of length %u.', obj.idx.P_h);
% Sigma_h
prior.Sigma_h = obj.options.prior.Sigma_h;
if isscalar(prior.Sigma_h)
prior.Sigma_h = eye(obj.idx.P_h)*prior.Sigma_h;
elseif isvector(prior.Sigma_h)
assert(length(prior.Sigma_h)>=4, 'TAPAS:HUGE:Prior', ...
'Prior homogenous covariance must be a vector of length 4.')
A = double(obj.dcm.a - diag(diag(obj.dcm.a)))*prior.Sigma_h(2) + ...
eye(obj.R)*prior.Sigma_h(1);
tmp = double([obj.dcm.b(:);obj.dcm.c(:);obj.dcm.d(:)])*prior.Sigma_h(3);
tmp = [A(:); tmp; ones(2*obj.R+1,1)*prior.Sigma_h(4)];
prior.Sigma_h = diag(tmp(obj.idx.homogenous));
end
try
assert(size(prior.Sigma_h, 1) == obj.idx.P_h && ...
issymmetric(prior.Sigma_h), '', '');
chol(prior.Sigma_h);
catch
error('TAPAS:HUGE:Prior', ['Prior hemodynamic covariance must ' ...
'be symmetric, positive definite matrix of size %u.'], obj.idx.P_h);
end
% mean and variance of lambda
prior.mu_lambda = obj.options.prior.mu_lambda;
assert(prior.mu_lambda > 0, 'TAPAS:HUGE:Prior', ...
'Prior mean of noise precision must be positive scalar.');
prior.s2_lambda = obj.options.prior.s2_lambda;
assert(prior.s2_lambda > 0, 'TAPAS:HUGE:Prior', ...
'Prior variance of noise precision must be positive scalar.');
% m_beta_0
prior.m_beta_0 = obj.options.prior.m_beta_0;
if isscalar(prior.m_beta_0)
prior.m_beta_0 = repmat(prior.m_beta_0, obj.M, 1);
end
assert(size(prior.m_beta_0, 1) == obj.M && ...
size(prior.m_beta_0, 2) == 1, 'TAPAS:HUGE:Prior', ...
'Prior confound mean must be column vector of length %u.', obj.M);
% S_beta_0
prior.S_beta_0 = obj.options.prior.S_beta_0;
if isscalar(prior.S_beta_0)
prior.S_beta_0 = eye(obj.M)*prior.S_beta_0;
end
try
assert(size(prior.S_beta_0, 1) == obj.M && ...
issymmetric(prior.S_beta_0), '', '');
chol(prior.S_beta_0);
catch
error('TAPAS:HUGE:Prior', ['Prior confound covariance must ' ...
'be symmetric, positive definite matrix of size %u.'], obj.M);
end
end
function [ obj ] = prepare_starting_values( obj )
% subject-level
% initialize starting values to prior mean
stVal1 = repmat([obj.prior.m_0, obj.prior.mu_h], obj.N, 1);
if ~isempty(obj.options.nvp.startingvaluedcm) && ...
isnumeric(obj.options.nvp.startingvaluedcm) % custom starting values
stVal1 = obj.options.nvp.startingvaluedcm;
[stN, stP] = size(stVal1);
assert(stN == obj.N && stP == obj.idx.P_c + obj.idx.P_h,...
'TAPAS:HUGE:StartingValues',...
'Size of subject-level starting values does not agree with model.')
elseif strcmpi(obj.options.nvp.startingvaluedcm, 'spm') % use SPM estimates
for n = 1:obj.N
try
tmp = [obj.data(n).spm.A(:); obj.data(n).spm.B(:); ...
obj.data(n).spm.C(:); obj.data(n).spm.D(:); ...
obj.data(n).spm.transit(:); obj.data(n).spm.decay(:);...
obj.data(n).spm.epsilon(:)];
stVal1(n,:) = full(tmp([obj.idx.clustering; obj.idx.homogenous]));
catch
fprintf('Missing or invalid SPM estimate for subject %u.' , n);
fprintf(' Using prior as starting value instead.\n');
end
end
end
% randomize starting values
if obj.options.nvp.randomize && ~(strcmpi(obj.options.nvp.method, 'MH') && ...
~obj.options.mh.nSteps.dcm) %%% TODO remove exception and introduce new method for clustering only
stVal1 = stVal1 + randn(size(stVal1)).*obj.options.start.dcm;
end
obj.options.start.subjects = stVal1;
% cluster-level
if ~isempty(obj.options.nvp.startingvaluegmm) && ...
isnumeric(obj.options.nvp.startingvaluegmm) % custom starting values
stVal2 = obj.options.nvp.startingvaluegmm;
[stN, stP] = size(stVal2);
assert(stN == obj.K && stP == obj.idx.P_c,...
'TAPAS:HUGE:StartingValues',...
'Size of cluster-level starting values does not agree with model.')
else
stVal2 = repmat(obj.prior.m_0, obj.K, 1);
end
% randomize starting values
if obj.options.nvp.randomize && ~(strcmpi(obj.options.nvp.method, 'MH') && ...
~obj.options.mh.nSteps.clusters)
stVal2 = stVal2 + randn(size(stVal2)).*obj.options.start.gmm;
end
obj.options.start.clusters = stVal2;
end