Skip to content

Commit

Permalink
Major updates to HRF_Est_Toolbox2 to allow passing in of SPM.mat file…
Browse files Browse the repository at this point in the history
…s to estimate the HRF
  • Loading branch information
Michael-Sun committed Feb 27, 2024
1 parent ae41e6d commit 1d1cdf6
Show file tree
Hide file tree
Showing 11 changed files with 1,072 additions and 358 deletions.
700 changes: 433 additions & 267 deletions CanlabCore/HRF_Est_Toolbox2/EstHRF_inAtlas/EstimateHRF_inAtlas.m

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions CanlabCore/HRF_Est_Toolbox2/EstHRF_inAtlas/describeHRF.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ function describeHRF(HRF)
% ::
% describeHRF(HRF_structure)

for c = 1:numel(HRF.params.CondNames)
for c = 1:numel(HRF.CondNames)
for t = 1:numel(HRF.types)
for r = 1:numel(HRF.region)

disp(['The ', HRF.types{t}, ' fitted ', HRF.region{r}, ' features ', num2str(numel(HRF.fit{t}{r}.(HRF.params.CondNames{c}).phases)), ' phases of activity for the ', HRF.params.CondNames{c}, ' condition']);
for p = 1:numel(HRF.fit{t}{r}.(HRF.params.CondNames{c}).phases)
if HRF.fit{t}{r}.(HRF.params.CondNames{c}).phase(p).peaks>0
disp(['Phase ' num2str(p), ', spanning TRs ', num2str(HRF.fit{t}{r}.(HRF.params.CondNames{c}).phases{p}), ' features ', num2str(HRF.fit{t}{r}.(HRF.params.CondNames{c}).phase(p).peaks), ' peak(s), (at TR(s) ', num2str(HRF.fit{t}{r}.(HRF.params.CondNames{c}).phase(p).time_to_peak), ...
' and features an AUC of ' num2str(HRF.fit{t}{r}.(HRF.params.CondNames{c}).phase(p).auc), ' (voxel-normed: ', num2str(HRF.fit{t}{r}.(HRF.params.CondNames{c}).phase(p).auc_voxnormed), ').']);
disp(['The ', HRF.types{t}, ' fitted ', HRF.region{r}, ' features ', num2str(numel(HRF.fit{t}{r}.(HRF.CondNames{c}).phases)), ' phases of activity for the ', HRF.CondNames{c}, ' condition']);
for p = 1:numel(HRF.fit{t}{r}.(HRF.CondNames{c}).phases)
if HRF.fit{t}{r}.(HRF.CondNames{c}).phase(p).peaks>0
disp(['Phase ' num2str(p), ', spanning TRs ', num2str(HRF.fit{t}{r}.(HRF.CondNames{c}).phases{p}), ' features ', num2str(HRF.fit{t}{r}.(HRF.CondNames{c}).phase(p).peaks), ' peak(s), (at TR(s) ', num2str(HRF.fit{t}{r}.(HRF.CondNames{c}).phase(p).time_to_peak), ...
' and features an AUC of ' num2str(HRF.fit{t}{r}.(HRF.CondNames{c}).phase(p).auc), ' (voxel-normed: ', num2str(HRF.fit{t}{r}.(HRF.CondNames{c}).phase(p).auc_voxnormed), ').']);
end
if HRF.fit{t}{r}.(HRF.params.CondNames{c}).phase(p).troughs>0
disp(['Phase ' num2str(p), ', spanning TRs ', num2str(HRF.fit{t}{r}.(HRF.params.CondNames{c}).phases{p}), ' features ', num2str(HRF.fit{t}{r}.(HRF.params.CondNames{c}).phase(p).troughs), ' trough(s), (at TR(s) ', num2str(HRF.fit{t}{r}.(HRF.params.CondNames{c}).phase(p).time_to_peak), ...
' and features an AUC of ' num2str(HRF.fit{t}{r}.(HRF.params.CondNames{c}).phase(p).auc), ' (voxel-normed: ', num2str(HRF.fit{t}{r}.(HRF.params.CondNames{c}).phase(p).auc_voxnormed), ').']);
if HRF.fit{t}{r}.(HRF.CondNames{c}).phase(p).troughs>0
disp(['Phase ' num2str(p), ', spanning TRs ', num2str(HRF.fit{t}{r}.(HRF.CondNames{c}).phases{p}), ' features ', num2str(HRF.fit{t}{r}.(HRF.CondNames{c}).phase(p).troughs), ' trough(s), (at TR(s) ', num2str(HRF.fit{t}{r}.(HRF.CondNames{c}).phase(p).time_to_peak), ...
' and features an AUC of ' num2str(HRF.fit{t}{r}.(HRF.CondNames{c}).phase(p).auc), ' (voxel-normed: ', num2str(HRF.fit{t}{r}.(HRF.CondNames{c}).phase(p).auc_voxnormed), ').']);
end
end

Expand Down
123 changes: 123 additions & 0 deletions CanlabCore/HRF_Est_Toolbox2/EstHRF_inAtlas/extractHRF.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
function [HRF, tc] = extractHRF(HRF_OBJ, CondNames, at, rois)
% Passed in HRF_OBJ is a cell array of fmri_data for each condition.
% CondNames should be a cell array of charstr for each condition


% Initialize the parallel pool if it's not already running
if isempty(gcp('nocreate'))
parpool;
end

% Preallocation of tc_local before the parfor loop
HRF= cell(1, numel(rois));
tc= cell(1, numel(rois));

CondNames=matlab.lang.makeValidName(CondNames);

% Now, handle the fourth dimension which varies with 'd'
numCondNames = numel(CondNames);

% HRF_local{d} = cell(1, numel(rois));
% tc_local{d} = cell(1, numel(rois));

% Consider doing apply_parcellation instead of mean(apply_mask(HRF_OBJ{c}, at.select_atlas_subset(rois(r), 'exact')).dat);
% [parcel_means, parcel_pattern_expression, parcel_valence, rmsv_pos, rmsv_neg] = apply_parcellation(dat,at);
% nps=load_image_set('npsplus');
% nps = get_wh_image(nps,1);
% [parcel_means, parcel_pattern_expression, parcel_valence, rmsv_pos, rmsv_neg] = apply_parcellation(dat,at, 'pattern_expression', nps);
% r=region(at,'unique_mask_values');
% wh_parcels=~all(isnan(parcel_means))

for r=1:numel(rois)
HRF{r} = struct;
tc{r} = cell(1, numCondNames);
tic
for c=1:numel(CondNames)

try
tc{r}{c}=mean(apply_mask(HRF_OBJ{c}, at.select_atlas_subset(rois(r), 'exact')).dat);

HRF{r}.(CondNames{c}).model=tc{r}{c};
[HRF{r}.(CondNames{c}).peaks, HRF{r}.(CondNames{c}).troughs]=detectPeaksTroughs(tc{r}{c}', false);

[~, regionVoxNum, ~, ~]=at.select_atlas_subset(rois(r), 'exact').get_region_volumes;
HRF{r}.(CondNames{c}).model_voxnormed=tc{r}{c}/regionVoxNum;
[HRF{r}.(CondNames{c}).peaks_voxnormed, HRF{r}.(CondNames{c}).troughs_voxnormed]=detectPeaksTroughs(tc{r}{c}'/regionVoxNum, false);
catch
% disp(t);
disp(c);
disp(rois{r});
tc{r}{c}
mean(apply_mask(HRF_OBJ{c}, at.select_atlas_subset(rois(r), 'exact')).dat)
{apply_mask(HRF_OBJ{c}, at.select_atlas_subset(rois(r), 'exact')).dat}
HRF_OBJ

end

% Number of phases
start_times = [HRF{r}.(CondNames{c}).peaks.start_time, HRF{r}.(CondNames{c}).troughs.start_time];
end_times = [HRF{r}.(CondNames{c}).peaks.end_time, HRF{r}.(CondNames{c}).troughs.end_time];
phases = [start_times(:), end_times(:)];
unique_phases = unique(phases, 'rows');
HRF{r}.(CondNames{c}).phases = mat2cell(unique_phases, ones(size(unique_phases, 1), 1), 2);

% Loop over phases
for p = 1:numel(HRF{r}.(CondNames{c}).phases)
features = {'peaks', 'troughs'};
for f = 1:2
feat = features{f};

% Count the number of features (peaks or troughs)
start_times = cell2mat({HRF{r}.(CondNames{c}).(feat).start_time});
current_phase_start = HRF{r}.(CondNames{c}).phases{p}(1);
HRF{r}.(CondNames{c}).phase(p).(feat) = sum(start_times == current_phase_start);

if HRF{r}.(CondNames{c}).phase(p).(feat) > 0
display(['Estimating ' , '_', rois{r}, '_', CondNames{c}, '_', ' Now...!']);
display(['Phase ' , num2str(p), 'Feature ', feat]);
idx = find(start_times == current_phase_start);
auc = cell2mat({HRF{r}.(CondNames{c}).(feat).AUC});
height = cell2mat({HRF{r}.(CondNames{c}).(feat).height});
time_to_peak = cell2mat({HRF{r}.(CondNames{c}).(feat).time_to_peak});
half_height = cell2mat({HRF{r}.(CondNames{c}).(feat).half_height});

feat_voxnormed = strcat(feat, '_voxnormed');
auc_voxnormed = cell2mat({HRF{r}.(CondNames{c}).(feat_voxnormed).AUC});
height_voxnormed = cell2mat({HRF{r}.(CondNames{c}).(feat_voxnormed).height});
time_to_peak_voxnormed = cell2mat({HRF{r}.(CondNames{c}).(feat_voxnormed).time_to_peak});
half_height_voxnormed = cell2mat({HRF{r}.(CondNames{c}).(feat_voxnormed).half_height});

HRF{r}.(CondNames{c}).phase(p).auc = unique(auc(idx));
HRF{r}.(CondNames{c}).phase(p).auc_voxnormed = unique(auc_voxnormed(idx));

HRF{r}.(CondNames{c}).phase(p).height = height(idx);
HRF{r}.(CondNames{c}).phase(p).height_voxnormed = height_voxnormed(idx);
HRF{r}.(CondNames{c}).phase(p).time_to_peak = time_to_peak(idx);
HRF{r}.(CondNames{c}).phase(p).time_to_peak_voxnormed = time_to_peak_voxnormed(idx);
HRF{r}.(CondNames{c}).phase(p).half_height = half_height(idx);
HRF{r}.(CondNames{c}).phase(p).half_height_voxnormed = half_height_voxnormed(idx);
end
end
end
end
display(strjoin({' Done in ', num2str(toc), ' seconds with ', rois{r}}));

end


% temp_HRF_fit = HRF_local;
% Save the results for this ROI
% display([num2str(t), ' Done!'])
% temp_HRF_fit{t} = HRF_local;
% tc{t} = tc;


% Transfer the results from the temporary cell array to the HRF structure
% HRF.fit = temp_HRF_fit;
% HRF.params=HRF_PARAMS;

delete(gcp('nocreate'));



end
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function Condition=generateConditionTS(fmri_d, conditions, onsets, durations)
function Condition=generateConditionTS(fmri_d, conditions, onsets, durations, varargin)
% Helper script to create condition vectors for hrf_fit_one_voxel()
% Michael Sun, Ph.D.
% - Takes fmri_data() object or the number of TRs in a 4D object.
Expand All @@ -22,6 +22,12 @@
SPIKES=1;
SPIKETRAINS=0;
end

if ~isempty(varargin)
SPIKETRAINS=1;
else
SPIKES=1;
end

if strcmp(class(fmri_d), 'fmri_data')
n=size(fmri_d.dat,2);
Expand Down
24 changes: 12 additions & 12 deletions CanlabCore/HRF_Est_Toolbox2/EstHRF_inAtlas/generateHRFTable.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@
tblData = cell(0, 10); % You may need to adjust the number of columns based on the data you're storing

% Iterate through the nested loops as before
for c = 1:numel(HRF.params.CondNames)
for c = 1:numel(HRF.CondNames)
for r = 1:numel(HRF.region)
for t = 1:numel(HRF.types)

%disp(['Number of phases: ', num2str(numel(HRF.fit{t}{r}.(HRF.params.CondNames{c}).phases))]);
%for p = 1:numel(HRF.fit{t}{r}.(HRF.params.CondNames{c}).phases)
%disp(['Number of phases: ', num2str(numel(HRF.fit{t}{r}.(HRF.CondNames{c}).phases))]);
%for p = 1:numel(HRF.fit{t}{r}.(HRF.CondNames{c}).phases)

% Define the basic information
type = HRF.types{t};
region = HRF.region{r};
condition = HRF.params.CondNames{c};
condition = HRF.CondNames{c};
phase_num = 0;
phase_span = [0 0];
peaks = 0;
Expand All @@ -31,20 +31,20 @@
auc_voxnormed = 0;


if numel(HRF.fit{t}{r}.(HRF.params.CondNames{c}).phases) > 1
for p = 1:numel(HRF.fit{t}{r}.(HRF.params.CondNames{c}).phases)
if numel(HRF.fit{t}{r}.(HRF.CondNames{c}).phases) > 1
for p = 1:numel(HRF.fit{t}{r}.(HRF.CondNames{c}).phases)

phase_num = p;
phase_span = HRF.fit{t}{r}.(HRF.params.CondNames{c}).phases{p};
phase_span = HRF.fit{t}{r}.(HRF.CondNames{c}).phases{p};

% Check for peaks and troughs
peaks = HRF.fit{t}{r}.(HRF.params.CondNames{c}).phase(p).peaks;
troughs = HRF.fit{t}{r}.(HRF.params.CondNames{c}).phase(p).troughs;
peaks = HRF.fit{t}{r}.(HRF.CondNames{c}).phase(p).peaks;
troughs = HRF.fit{t}{r}.(HRF.CondNames{c}).phase(p).troughs;
%disp(['Peaks: ', num2str(peaks), ', Troughs: ', num2str(troughs)]);
% Extract other data
time_to_peak = HRF.fit{t}{r}.(HRF.params.CondNames{c}).phase(p).time_to_peak;
auc = HRF.fit{t}{r}.(HRF.params.CondNames{c}).phase(p).auc;
auc_voxnormed = HRF.fit{t}{r}.(HRF.params.CondNames{c}).phase(p).auc_voxnormed;
time_to_peak = HRF.fit{t}{r}.(HRF.CondNames{c}).phase(p).time_to_peak;
auc = HRF.fit{t}{r}.(HRF.CondNames{c}).phase(p).auc;
auc_voxnormed = HRF.fit{t}{r}.(HRF.CondNames{c}).phase(p).auc_voxnormed;
% Append the data to the cell array
tblData = [tblData; {type, region, condition, phase_num, phase_span, peaks, troughs, time_to_peak, auc, auc_voxnormed}];
end
Expand Down
164 changes: 164 additions & 0 deletions CanlabCore/HRF_Est_Toolbox2/EstHRF_inAtlas/hrf_fit.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
function [params_obj, hrf_obj, params_obj_dat, hrf_obj_dat, info] = hrf_fit(SPM,T,method,mode)
% HRF estimation on fmri_data class object
%
% HRF estimation function for a single voxel;
%
% Implemented methods include: IL-model (Deterministic/Stochastic), FIR
% (Regular/Smooth), and HRF (Canonical/+ temporal/+ temporal & dispersion).
% With SPM.mat, TR, and experimental design are imported.
%
% :Inputs:
%
% **SPM**
% SPM.mat file
%
% **T**
% length of estimated HRF ij seconds
%
% **type**
% Model type: 'FIR', 'IL', or 'CHRF'
%
% **mode**
% Mode
%
% :Model Types:
%
% A. **Fit HRF using IL-function**
% Choose mode (deterministic/stochastic)
% - 0 - deterministic aproach
% - 1 - simulated annealing approach
%
% Please note that when using simulated annealing approach you
% may need to perform some tuning before use.
%
% B. **Fit HRF using FIR-model**
% Choose mode (FIR/sFIR)
% - 0 - FIR
% - 1 - smooth FIR
%
% C. **Fit Canonical HRF**
% Choose mode (FIR/sFIR)
% - 0 - FIR
% - 1 - smooth FIR
%
%
% ..
% Created by Michael Sun on 02/20/24
% ..

if isstring(SPM) || ischar(SPM)

load(SPM);
if ~exist('SPM', 'var')
error('Passed in filepath is not an SPM.mat file.')
end

end


if isstruct(SPM)
fnames=unique({SPM.xY.VY.fname})';

parfor i=1:numel(fnames)

if contains(fnames{i}, '/') && ispc
% PC Path conversion from Unix
if strcmp(fnames{i}(1,1:2), '\\')
d{i}=fmri_data(strrep(fnames{i}, '/', '\'));
else
d{i}=fmri_data(['\',strrep(fnames{i}, '/', '\')]);
end

else
d{i}=fmri_data(fnames{i});
end
end

% Transform timeseries data into the way SPM desires.
gkwy_d=spmify(d,SPM);

% Extract TR
TR = SPM.xY.RT;

if isempty(T)
disp('T is empty, generating T as 2 times the maximum duration for each task regressor.');
for i = 1:numel(SPM.Sess)
T{i}=cellfun(@(cellArray) 2*ceil(max(cellArray)), {SPM.Sess(i).U.dur});
end
elseif numel(SPM.Sess)>1 && ~iscell(T)
% error('SPM structures concatenating multiple runs must have time windows passed in with a matching number of cell arrays.');
T=repmat({T}, 1, numel(SPM.Sess));
elseif iscell(T) && numel(SPM.Sess)~=numel(T)
error('Incompatible number of runs in SPM and in cell-array T.')
end

% %% ONE WAY
% % run hrf_fit separately for every d
for i=1:numel(d)
% Reassign data.
d{i}.dat=gkwy_d{i};

% Extract TR
TR = SPM.xY.RT;
Runc=generateConditionTS(numel(SPM.Sess(i).row), [SPM.Sess(i).U.name], {SPM.Sess(i).U.ons}, {SPM.Sess(i).U.dur});

% There's a need to pull apart the SPM design matrix for every run
% Column of regressors + covariates for each session + intercept
% X=SPM.xX.X(SPM.Sess(i).row, [SPM.Sess(i).col, SPM.xX.iB(i)]);

% Trouble is, X is not filtered
% X=spm_filter(SPM.xX.K(i), SPM.xX.X(SPM.Sess(i).row, [SPM.Sess(i).col, SPM.xX.iB(i)]));

% Trouble here is, task regressors may have to be re-generated for
% FIR and sFIR if the original design matrix is cHRF, so check the
% basis function
if strcmpi(method, 'FIR') && ~contains(SPM.xBF.name, 'FIR')
disp('Passed in SPM structure is not FIR. Reconstructing task-regressors for FIR fit');

len = numel(SPM.Sess(i).row);

% Task regressors for each run can be found here:
numstim=numel(SPM.Sess(i).U);

% Make the design matrix:
DX_all = cell(1, numstim); % Store DX matrices for each condition
tlen_all = zeros(1, numstim); % Store tlen for each condition

for s=1:numstim
t = 1:TR:T{i}(s);
tlen_all(s) = length(t);
DX_all{s} = tor_make_deconv_mtx3(Runc(:,s), tlen_all(s), 1);
end
DX = horzcat(DX_all{:});

% due to horzcat, we will have multiple intercepts in this design matrix
% therefore, we'll find the intercepts, drop them, and add an intercept at the end of the matrix
intercept_idx = find(sum(DX)==len);
copyDX = DX;
copyDX(:,intercept_idx) = [];
DX = [copyDX ones(len,1)];

% Covariate Design Matrix for each session (without intercept):
NX=[SPM.Sess(i).C.C];

% Concatenate the Task regressors with Covariates
X=[DX, NX];
% Filter
X=spm_filter(SPM.xX.K(i), X);

else
% Otherwise set X to be the filtered design matrix of that
% section.
X=spm_filter(SPM.xX.K(i), SPM.xX.X(SPM.Sess(i).row, [SPM.Sess(i).col, SPM.xX.iB(i)]));
end

[params_obj{i}, hrf_obj{i}, params_obj_dat{i}, hrf_obj_dat{i}] = hrf_fit(d{i},TR,Runc,T{i},method,mode,X);

info{i}.X=X;
info{i}.names=[SPM.Sess(i).U.name]';

end

end

end
Loading

0 comments on commit 1d1cdf6

Please sign in to comment.