├── .gitignore ├── LICENSE ├── README.md └── scripts ├── +utils ├── baseline_correct.m ├── check_preproc_data.m ├── check_raw_data.m ├── get_studydetails.m ├── load_epoch_results.m ├── merge_sessions.m ├── nnmf_mww.m ├── run_flame.m ├── run_glm.m ├── run_group_glm.m ├── run_nnmf.m ├── set1_cols.m └── set_redblue_colourmap.m ├── hmm_0_initialise.m ├── hmm_1_preprocessing.m ├── hmm_2_envelope_estimation.m ├── hmm_3_embedded_estimation.m ├── hmm_4_envelope_results.m └── hmm_5_embedded_results.m /.gitignore: -------------------------------------------------------------------------------- 1 | toolboxes/ 2 | *DS_Store 3 | *.swp 4 | *.swo 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 OHBA Analysis Group 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HMM Analysis on Wakeman & Henson Dataset 2 | 3 | The repository contains the scripts to run two different types of Hidden Markov Model analysis on a freely available MEG dataset. The analyses are described in full in our accompanying paper 4 | 5 | Quinn, A. J., Vidaurre, D., Abeysuriya, R., Becker, R., Nobre, A. C., & Woolrich, M. W. (2018). Task-Evoked Dynamic Network Analysis Through Hidden Markov Modeling. Frontiers in Neuroscience, 12. https://doi.org/10.3389/fnins.2018.00603 6 | 7 | This was published as part of a Frontiers in Neuroscience special issue - "From raw MEG/EEG to publication: how to perform MEG/EEG group analysis with free academic software." (https://www.frontiersin.org/research-topics/5158/from-raw-megeeg-to-publication-how-to-perform-megeeg-group-analysis-with-free-academic-software) 8 | 9 | ### Prerequisites 10 | 11 | To run these analyses you will require: 12 | 13 | - A UNIX-type computer system 14 | - FSL version 5.0.9 (https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/FslInstallation) 15 | - MatLab 2014b or greater 16 | - SPM12 MatLab Toolbox (http://www.fil.ion.ucl.ac.uk/spm/software/download/) 17 | - OSL2 MatLab Toolbox (https://ohba-analysis.github.io/osl-docs/) 18 | - HMM-MAR MatLab toolbox (https://github.com/OHBA-analysis/HMM-MAR) 19 | 20 | OSL2 and HMM-MAR are provided as part of this download. SPM12 should be installed into the osl2 directory. 21 | 22 | ### Getting Started 23 | 24 | The download containing core scripts and associated toolboxes can be found on our OSF page here: https://osf.io/ugjbr/ 25 | 26 | Please download the HMM\_Task\_Download.zip file. It is recommended to make sure the scripts subdirectory in the download folder contains the latest versions of the scripts from this repository. 27 | 28 | Note: All folder paths referred to here are relative to the location of this download, ie all paths assume that your current directory is the top-level of this download. 29 | 30 | Once the download is complete take a look into the toolboxes folder of this download. You will find a copy of HMM-MAR (github repo: https://github.com/OHBA-analysis/HMM-MAR) and OSL (docs: https://ohba-analysis.github.io/osl-docs/). These are the core MatLab toolboxes that will run the analyses. 31 | 32 | Next, take a look into the scripts/ folder within the HMM download. This contains the main analysis script and a utils subfolder containing a small helper module with some useful functions used within the analysis. 33 | 34 | Get the data: 35 | 36 | 1) Download revision 0.1.1 of the Wakeman and Henson dataset (http://doi.org/10.1038/sdata.2015.1). These files can be downloaded anywhere on your system which is convenient and that can be accessed through your MatLab file browser. 37 | 38 | Install SPM12 and OSL (note these steps can be carried out whilst the data is downloading): 39 | 40 | 2) Download SPM12 and install into the toolboxes/osl folder, SPM is available here: http://www.fil.ion.ucl.ac.uk/spm/software/spm12/ 41 | 42 | 3) Install OSL. Additional information about the dependencies and install path for OSL can be found in toolboxes/osl/README.md and toolboxes/osl/osl-core/README.md 43 | 44 | Configure the analysis: 45 | 46 | 4) Open scripts/+utils/get\_studyinfo.m. This script defines the locations of the downloaded data, analysis scripts and the location to store results and is used throughout the analysis to load and save data. Edit the file paths to match the chosen locations on your system. 47 | 48 | 5) open scripts/hmm\_0\_initialise.m . This script adds all the relevant paths to your MatLab session and loads the information defined within get\_studyinfo.m . Edit the download\_path at the top to the location of this download. 49 | 50 | Check installation 51 | 52 | 6) Open MatLab 2014b (or greater) and run hmm\_0\_initialise.m toolboxes to the path and load the information from get\_studyinfo.m . 53 | 54 | 7) Run utils.check\_raw\_data in your MatLab session. This should confirm that 'All raw data is found in studydir'. If not, check that the studydir path in scripts/+utils/get\_studyinfo.m matches the location of your downloaded data. 55 | 56 | 8) Run osl\_check\_installation in your MatLab session. This will print a range of information about your OSL and MatLab environment and highlight any missing dependencies. 57 | 58 | ### Main Analysis 59 | 60 | The HMM analysis is completed in a MatLab session in which the hmm\_0\_initialise.m script has been excecuted. 61 | 62 | The HMM analysis proceeds by following the hmm\_\* scripts within the scripts/ subfolder of this download. They should be run in order, though each script will define and load everything it needs. So you can exit matlab and start a new session in between running two of the scripts as long as hmm\_0\_initialise.m is excecuted in each new session. 63 | 64 | The hmm\_\* scripts contain additional information and explanations which help to describe the preprocessing and analysis as it is run. 65 | -------------------------------------------------------------------------------- /scripts/+utils/baseline_correct.m: -------------------------------------------------------------------------------- 1 | function [data] = baseline_correct( data, baseline_inds ) 2 | %%function [data] = baseline_correct( data, baseline_inds ) 3 | % 4 | % baseline corrects the second dimenion 5 | 6 | for ii = 1:size(data,1) 7 | for jj = 1:size(data,4) 8 | bl = squeeze(nanmean(nanmean(data(ii,baseline_inds,:,jj),2),3)); 9 | data(ii,:,:,jj) = data(ii,:,:,jj) - bl; 10 | end 11 | end -------------------------------------------------------------------------------- /scripts/+utils/check_preproc_data.m: -------------------------------------------------------------------------------- 1 | function out = check_preproc_data() 2 | %%function [missing_files,missing_proc_files] = check_preproc_data() 3 | % 4 | % Helper function to check that the preprocessing has completed successfully 5 | 6 | % Get study information 7 | config = utils.get_studydetails(); 8 | 9 | % Preallocate bad file arrays 10 | out = []; 11 | out.missing_files = {}; 12 | out.missing_proc_files = {}; 13 | out.missing_epochinfo = {}; 14 | 15 | % Main loop 16 | for subj = 1:19 17 | fprintf('Checking subject: %d\n',subj); 18 | for run = 1:6 19 | 20 | % Generate run name 21 | runname = sprintf( 'spm_sub%02d_run_%02d.mat', subj,run ); 22 | 23 | %% Check spm_sss directory 24 | rawpath = fullfile( config.analysisdir, 'spm_sss',runname ); 25 | if ~exist( rawpath ) 26 | out.missing_files{end+1} = rawpath; 27 | continue 28 | end 29 | D = spm_eeg_load(rawpath); 30 | 31 | % Check coregistration has run 32 | if ~isfield( D, 'inv' ) 33 | fprintf( 'Coreg out.missing for file: %s\n',runpath); 34 | end 35 | 36 | %% Check spm_sss_preprocessed directory 37 | preprocpath = fullfile( config.analysisdir, 'spm_sss_processed',runname ); 38 | if ~exist( preprocpath ) 39 | out.missing_files{end+1} = preprocpath; 40 | continue 41 | end 42 | D = spm_eeg_load(preprocpath); 43 | 44 | % Check that we have 5 montages 45 | if D.montage('getnumber') ~= 5 46 | out.missing_proc_files{end+1} = preprocpat; 47 | end 48 | 49 | % Check that the fifth montage has 39 channels 50 | D = D.montage('switch',5); 51 | if D.nchannels ~= 39 52 | out.missing_proc_files{end+1} = preprocpat; 53 | end 54 | 55 | end 56 | end 57 | 58 | fprintf('\n\n'); 59 | 60 | %% Check epoch information 61 | load( fullfile( config.analysisdir,'spm_sss_processed','epochinfo.mat') ); 62 | missing_epochs = cellfun( @isempty, epochinfo ); 63 | if sum( missing_epochs ) == 0 64 | disp('All epochinfo present in epochinfo file'); 65 | elseif sum( missing_epochs ) > 0 66 | msg = sprintf( '%d subjects epochinfo out.missing', length(find(missing_epochs)) ); 67 | out.missing_epochs = find(missing_epochs); 68 | end 69 | 70 | % Inform user utils.ther files are out.missing. 71 | if isempty( out.missing_files) 72 | disp('All coregistered data found in spm_sss dir'); 73 | else 74 | msg = sprintf( '%d raw data files out.missing', length(out.missing_files) ); 75 | warning(msg); 76 | for ii = 1:length(out.missing_files) 77 | disp(out.missing_files{ii}); 78 | end 79 | end 80 | 81 | % Inform user utils.ther files are out.missing. 82 | if isempty( out.missing_proc_files) 83 | disp('All parcel-network data found in spm_sss_processed dir'); 84 | else 85 | msg = sprintf( '%d raw data files out.missing', length(out.missing_proc_files) ); 86 | warning(msg); 87 | for ii = 1:length(out.missing_proc_files) 88 | disp(out.missing_proc_files{ii}); 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /scripts/+utils/check_raw_data.m: -------------------------------------------------------------------------------- 1 | function missing_files = check_raw_data() 2 | 3 | config = utils.get_studydetails(); 4 | 5 | missing_files = {}; 6 | for subj = 1:19 7 | 8 | subjname = sprintf( 'sub0%02d', subj ); 9 | subjpath = fullfile(config.datadir,'raw_data',subjname); 10 | 11 | % Check fif locations 12 | for run = 1:6 13 | runpath = fullfile( subjpath, sprintf( 'run_0%d_sss.fif', run ) ); 14 | if ~exist( runpath ) 15 | missing_files{end+1} = runpath; 16 | end 17 | end 18 | 19 | % Check structural location 20 | runpath = fullfile( subjpath, 'highres001.nii' ); 21 | if ~exist( runpath ) 22 | missing_files{end+1} = runpath; 23 | end 24 | 25 | end 26 | 27 | % Inform user whether files are missing. 28 | if isempty( missing_files) 29 | disp('All raw data found in studydir'); 30 | else 31 | msg = sprintf( '%d raw data files missing', length(missing_files) ); 32 | warning(msg); 33 | for ii = 1:length(missing_files) 34 | disp(missing_files{ii}); 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /scripts/+utils/get_studydetails.m: -------------------------------------------------------------------------------- 1 | function [config] = get_studydetails() 2 | 3 | config = []; 4 | 5 | % The location of the downloaded data 6 | % eg: config.datadir = '/home/disk3/ajquinn/ds000117/'; 7 | config.datadir = ''; % <- edit this line 8 | 9 | % The location of the downloaded scripts directory 10 | % config.scriptdir = '/home/disk3/ajquinn/HMM_frontiers_download/'; 11 | config.scriptdir = ''; % <- edit this line 12 | 13 | % The location to save the analyses generated by these scripts 14 | % config.analysisdir = '/home/disk3/ajquinn/HMM_face_analysis/'; 15 | config.analysisdir = ''; % <- edit this line 16 | 17 | % Run some sanity checks 18 | 19 | % Are the paths specified? 20 | if isempty(config.datadir) 21 | warning('Please specify the location of the datadir inside utils/+utils/get_studydetails.'); 22 | end 23 | if isempty(config.scriptdir) 24 | warning('Please specify the location of the scriptdir inside utils/+utils/get_studydetails.'); 25 | end 26 | if isempty(config.analysisdir) 27 | warning('Please specify the location of the analysisdir inside utils/+utils/get_studydetails.'); 28 | end 29 | 30 | % Do all the folders exist? 31 | if exist( config.datadir, 'dir' ) == 0 32 | error('datadir path in utils/+utils/get_studydetails.m not found: %s', config.datadir); 33 | end 34 | if exist( config.scriptdir, 'dir' ) == 0 35 | error('studydir path in utils/+utils/get_studydetails.m not found: %s', config.scriptdir); 36 | end 37 | if exist( config.analysisdir, 'dir' ) == 0 38 | error('analysisdir path in utils/+utils/get_studydetails.m not found: %s', config.analysisdir); 39 | end 40 | -------------------------------------------------------------------------------- /scripts/+utils/load_epoch_results.m: -------------------------------------------------------------------------------- 1 | function [ trial_data ] = load_epoch_results( data, epochinfo, runlen, B, R) 2 | 3 | % meta information 4 | nchannels = size(data,1); % this could also be states 5 | nsamples = epochinfo{1}.trl(1,2) - epochinfo{1}.trl(1,1) + 1; 6 | 7 | % return array 8 | trial_data = zeros(nchannels,nsamples,148,114)*nan; 9 | 10 | % main loop 11 | for ii = 1:19 12 | for jj = 1:6 13 | ind = ((ii-1)*6)+jj; 14 | 15 | % get bad samples 16 | good_inds=setdiff(1:runlen(ind),B{ind}); 17 | 18 | % extract subject data, accounting for bad samples 19 | subj_data = zeros( nchannels,runlen(ind) )*nan; 20 | subj_data(:,good_inds) = data(:,R(ind,1):R(ind,2)); 21 | 22 | % main epoch loop 23 | for kk = 1:size(epochinfo{ind}.trl,1) 24 | % get trial start and stop samples 25 | start = epochinfo{ind}.trl(kk,1); 26 | stop = epochinfo{ind}.trl(kk,2); 27 | 28 | if stop > size(subj_data,2) || start > size(subj_data,2) 29 | continue % not all sessions have 148 trials 30 | else 31 | trial_data(:,:,kk,ind) = subj_data(:,start:stop); 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /scripts/+utils/merge_sessions.m: -------------------------------------------------------------------------------- 1 | function data_merged = merge_sessions( data, session_subj_mapping ) 2 | %function data = merge_sessions( data, session_subj_mapping ) 3 | % 4 | % last dimension of data should be sessions 5 | % session_subj_mapping is repelem(1:19,6); 6 | 7 | nsubjs = length(unique( session_subj_mapping )); 8 | nsess = sum( session_subj_mapping==1 ); 9 | ntrials = size(data,3)*nsess; 10 | 11 | data_merged = zeros( size(data,1), size(data,2), ntrials, nsubjs ); 12 | 13 | for ii = 1:nsubjs 14 | 15 | data_merged(:,:,:,ii) = reshape( data(:,:,:,session_subj_mapping==ii), size(data,1),size(data,2),ntrials ); 16 | 17 | end 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /scripts/+utils/nnmf_mww.m: -------------------------------------------------------------------------------- 1 | function [wbest,hbest,normbest] = nnmf(a,k,varargin) 2 | %NNMF Non-negative matrix factorization. 3 | % [W,H] = NNMF(A,K) factors the N-by-M matrix A into non-negative factors 4 | % W (N-by-K) and H (K-by-M). The result is not an exact factorization, 5 | % but W*H is a lower-rank approximation to the original matrix A. The W 6 | % and H matrices are chosen to minimize the objective function that is 7 | % defined as the root mean squared residual between A and the 8 | % approximation W*H. This is equivalent to 9 | % 10 | % D = norm(A-W*H,'fro')/sqrt(N*M) 11 | % 12 | % The factorization uses an iterative method starting with random initial 13 | % values for W and H. Because the objective function often has local 14 | % minima, repeated factorizations may yield different W and H values. 15 | % Sometimes the algorithm converges to solutions of lower rank than K, 16 | % and this is often an indication that the result is not optimal. 17 | % 18 | % [W,H,D] = NNMF(...) also returns D, the root mean square residual. 19 | % 20 | % [W,H,D] = NNMF(A,K,'PARAM1',val1,'PARAM2',val2,...) specifies one or 21 | % more of the following parameter name/value pairs: 22 | % 23 | % Parameter Value 24 | % 'algorithm' Either 'als' (default) to use an alternating least 25 | % squares algorithm, or 'mult' to use a multiplicative 26 | % update algorithm. 27 | % 'w0' An N-by-K matrix to be used as the initial value for W. 28 | % 'h0' A K-by-M matrix to be used as the initial value for H. 29 | % 'replicates' The number of times to repeat the factorization, using 30 | % new random starting values for W and H, except at the 31 | % first replication if w0 and h0 are given (default 1). 32 | % This tends to be most beneficial with the 'mult' 33 | % algorithm. 34 | % 'options' An options structure as created by the STATSET 35 | % function. NNMF uses the following fields: 36 | % 37 | % 'Display' Level of display output. Choices are 'off' 38 | % (the default), 'final', and 'iter'. 39 | % 'MaxIter' Maximum number of steps allowed. The default 40 | % is 100. Unlike in optimization settings, 41 | % reaching MaxIter is regarded as convergence. 42 | % 'TolFun' Positive number giving the termination tolerance 43 | % for the criterion. The default is 1e-4. 44 | % 'TolX' Positive number giving the convergence threshold 45 | % for relative change in the elements of W and H. 46 | % The default is 1e-4. 47 | % 'UseParallel' 48 | % 'UseSubstreams' 49 | % 'Streams' These fields specify whether to perform multiple 50 | % replicates in parallel, and how to use random 51 | % numbers when generating the starting points for 52 | % the replicates. For information on these fields 53 | % see PARALLELSTATS. 54 | % NOTE: If 'UseParallel' is TRUE and 'UseSubstreams' is FALSE, 55 | % then the length of 'Streams' must equal the number of workers 56 | % used by NNMF. If a parallel pool is already open, this 57 | % will be the size of the parallel pool. If a parallel pool 58 | % is not already open, then MATLAB may try to open a pool for 59 | % you (depending on your installation and preferences). 60 | % To ensure more predictable results, it is best to use 61 | % the PARPOOL command and explicitly create a parallel pool 62 | % prior to invoking NNMF with 'UseParallel' set to TRUE. 63 | % 64 | % Examples: 65 | % % Non-negative rank-2 approximation of the Fisher iris measurements 66 | % load fisheriris 67 | % [w,h] = nnmf(meas,2); 68 | % gscatter(w(:,1),w(:,2),species); 69 | % hold on; biplot(max(w(:))*h','VarLabels',{'sl' 'sw' 'pl' 'pw'},'positive',true); hold off; 70 | % axis([0 12 0 12]); 71 | % 72 | % % Try a few iterations at several replicates using the 73 | % % multiplicative algorithm, then continue with more iterations 74 | % % from the best of these results using alternating least squares 75 | % x = rand(100,20)*rand(20,50); 76 | % opt = statset('maxiter',5,'display','final'); 77 | % [w,h] = nnmf(x,5,'rep',10,'opt',opt,'alg','mult'); 78 | % opt = statset('maxiter',1000,'display','final'); 79 | % [w,h] = nnmf(x,5,'w0',w,'h0',h,'opt',opt,'alg','als'); 80 | % 81 | % See also BIPLOT, PCA, STATSET, PARALLELSTATS. 82 | 83 | % Copyright 2007-2013 The MathWorks, Inc. 84 | 85 | % Reference: 86 | % M.W. Berry et al. (2007), "Algorithms and Applications for Approximate 87 | % Nonnegative Matrix Factorization," Computational Statistics and Data 88 | % Analysis, vol. 52, no. 1, pp. 155-173. 89 | 90 | % The factorization is not uniquely defined. This function normalizes H so 91 | % that its rows have unit length. It orders the columns of W and rows of H 92 | % so that the columns of W have decreasing length. 93 | 94 | % Check required arguments 95 | narginchk(2,Inf); 96 | [n,m] = size(a); 97 | if ~isscalar(k) || ~isnumeric(k) || k<1 || k>min(m,n) || k~=round(k) 98 | error(message('stats:nnmf:BadK')); 99 | end 100 | 101 | % Process optional arguments 102 | pnames = {'algorithm' 'w0' 'h0' 'replicates' 'options'}; 103 | dflts = {'als' [] [] 1 [] }; 104 | [alg,w0,h0,tries,options] = ... 105 | internal.stats.parseArgs(pnames,dflts,varargin{:}); 106 | 107 | % Check optional arguments 108 | alg = internal.stats.getParamVal(alg,{'mult' 'als'},'ALGORITHM'); 109 | ismult = strncmp('mult',alg,numel(alg)); 110 | checkmatrices(a,w0,h0,k); 111 | if ~isscalar(tries) || ~isnumeric(tries) || tries<1 || tries~=round(tries) 112 | error(message('stats:nnmf:BadReplicates')); 113 | end 114 | 115 | defaultopt = statset('nnmf'); 116 | tolx = statget(options,'TolX',defaultopt,'fast'); 117 | tolfun = statget(options,'TolFun',defaultopt,'fast'); 118 | maxiter = statget(options,'MaxIter',defaultopt,'fast'); 119 | dispopt = statget(options,'Display',defaultopt,'fast'); 120 | 121 | [~,dispnum] = internal.stats.getParamVal(dispopt, {'off','notify','final','iter'},'Display'); 122 | dispnum = dispnum - 1; 123 | 124 | [useParallel, RNGscheme, poolsz] = ... 125 | internal.stats.parallel.processParallelAndStreamOptions(options,true); 126 | 127 | usePool = useParallel && poolsz>0; 128 | 129 | % Special case, if K is full rank we know the answer 130 | if isempty(w0) && isempty(h0) 131 | if k==m 132 | w0 = a; 133 | h0 = eye(k); 134 | elseif k==n 135 | w0 = eye(k); 136 | h0 = a; 137 | end 138 | end 139 | 140 | 141 | % Define the function that will perform one iteration of the 142 | % loop inside smartFor 143 | loopbody = @loopBody; 144 | 145 | % Suppress undesired warnings. 146 | if usePool 147 | % On workers and client 148 | pctRunOnAll internal.stats.parallel.muteParallelStore('rankDeficientMatrix', ... 149 | warning('off','MATLAB:rankDeficientMatrix') ); 150 | else 151 | % On client 152 | ws = warning('off','MATLAB:rankDeficientMatrix'); 153 | end 154 | 155 | % Prepare for in-progress 156 | if dispnum > 1 % 'iter' or 'final' 157 | if usePool 158 | % If we are running on a parallel pool, each worker will generate 159 | % a separate periodic report. Before starting the loop, we 160 | % seed the parallel pool so that each worker will have an 161 | % identifying label (eg, index) for its report. 162 | internal.stats.parallel.distributeToPool( ... 163 | 'workerID', num2cell(1:poolsz) ); 164 | 165 | % Periodic reports behave differently in parallel than they do 166 | % in serial computation (which is the baseline). 167 | % We advise the user of the difference. 168 | warning(message('stats:nnmf:displayParallel2')); 169 | 170 | % Leave formatted by \t UI strings untranslated. 8/17/2011 171 | fprintf(' worker\t rep\t iteration\t rms resid\t |delta x|\n' ); 172 | else 173 | if useParallel 174 | warning(message('stats:nnmf:displayParallel')); 175 | end 176 | fprintf(' rep\t iteration\t rms resid\t |delta x|\n'); 177 | end 178 | end 179 | 180 | try 181 | whbest = internal.stats.parallel.smartForReduce(... 182 | tries, loopbody, useParallel, RNGscheme, 'argmin'); 183 | catch ME 184 | % Revert warning setting for rankDeficientMatrix to value prior to nnmf. 185 | if usePool 186 | % On workers and on client 187 | pctRunOnAll warning(internal.stats.parallel.statParallelStore('rankDeficientMatrix').state,'MATLAB:rankDeficientMatrix'); 188 | else 189 | % On client 190 | warning(ws); 191 | end 192 | rethrow(ME); 193 | end 194 | 195 | normbest = whbest{1}; 196 | wbest = whbest{3}; 197 | hbest = whbest{4}; 198 | % whbest{2} contains the iteration chosen for the best factorization, 199 | % but it has no meaning except as a "reproducible" tie-breaker, and 200 | % is not supplied as a return value. 201 | 202 | if dispnum > 1 % 'final' or 'iter' 203 | fprintf('%s\n',getString(message('stats:nnmf:FinalRMSResidual',sprintf('%g',normbest)))); 204 | end 205 | 206 | % Revert warning setting for rankDeficientMatrix to value prior to nnmf. 207 | if usePool 208 | % On workers and on client 209 | pctRunOnAll warning(internal.stats.parallel.statParallelStore('rankDeficientMatrix').state,'MATLAB:rankDeficientMatrix'); 210 | else 211 | % On client 212 | warning(ws); 213 | end 214 | 215 | if normbest==Inf 216 | error(message('stats:nnmf:NoSolution')) 217 | end 218 | 219 | % MWW Put the outputs in a standard form - first normalize h 220 | % note w is freq, h is space 221 | if 1 222 | % original code - puts amplitude in w 223 | hlen = sqrt(sum(hbest.^2,2)); 224 | else 225 | % puts amplitude in h 226 | hlen = 1./sqrt(sum(wbest.^2,1))'; 227 | end 228 | 229 | if any(hlen==0) 230 | warning(message('stats:nnmf:LowRank', k - sum( hlen==0 ), k)); 231 | hlen(hlen==0) = 1; 232 | end 233 | 234 | wbest = bsxfun(@times,wbest,hlen'); 235 | hbest = bsxfun(@times,hbest,1./hlen); 236 | 237 | % Then order by w 238 | [~,idx] = sort(sum(wbest.^2,1),'descend'); 239 | wbest = wbest(:,idx); 240 | hbest = hbest(idx,:); 241 | 242 | % ---- Nested functions ---- 243 | 244 | function cellout = loopBody(iter,S) 245 | if isempty(S) 246 | S = RandStream.getGlobalStream; 247 | end 248 | 249 | % whtry is a "temporary variable" and hence needs to be 250 | % reinitialized at start of each loop. 251 | whtry = cell(4,1); % whtry{1} = norm of error 252 | % whtry{3} = w 253 | % whtry{4} = h 254 | 255 | % Get random starting values if required 256 | if( ~isempty(w0) && iter ==1 ) 257 | whtry{3} = w0; 258 | else 259 | whtry{3} = rand(S,n,k); 260 | end 261 | if( ~isempty(h0) && iter ==1 ) 262 | whtry{4} = h0; 263 | else 264 | whtry{4} = rand(S,k,m); 265 | end 266 | 267 | % Perform a factorization 268 | [whtry{3},whtry{4},whtry{1}] = ... 269 | nnmf1(a,whtry{3},whtry{4},ismult,maxiter,tolfun,tolx,... 270 | dispnum,iter,usePool); 271 | whtry{2} = iter; 272 | 273 | cellout = whtry; 274 | end 275 | 276 | end % of nnmf 277 | 278 | % ------------------- 279 | function [w,h,dnorm] = nnmf1(a,w0,h0,ismult,maxiter,tolfun,tolx,... 280 | dispnum,repnum,usePool) 281 | % Single non-negative matrix factorization 282 | nm = numel(a); 283 | sqrteps = sqrt(eps); 284 | 285 | 286 | % Display progress. For parallel computing, the replicate number will be 287 | % displayed under the worker performing the replicate. 288 | if dispnum>1 % 'final' or 'iter' 289 | if usePool 290 | labindx = internal.stats.parallel.workerGetValue('workerID'); 291 | dispfmt = '%8d\t%8d\t%8d\t%14g\t%14g\n'; 292 | else 293 | dispfmt = '%7d\t%8d\t%12g\t%12g\n'; 294 | end 295 | end 296 | 297 | for j=1:maxiter 298 | % note w is freq, h is space 299 | 300 | if ismult 301 | % Multiplicative update formula 302 | numer = w0'*a; 303 | h = max(0,h0 .* (numer ./ ((w0'*w0)*h0 + eps(numer)))); 304 | numer = a*h'; 305 | w = max(0,w0 .* (numer ./ (w0*(h*h') + eps(numer)))); 306 | else 307 | % Alternating least squares 308 | h = max(0, w0\a); 309 | if maxiter>1 310 | w = max(0, a/h); 311 | else 312 | w=w0; 313 | end 314 | end 315 | 316 | % Get norm of difference and max change in factors 317 | d = a - w*h; 318 | dnorm = sqrt(sum(sum(d.^2))/nm); 319 | dw = max(max(abs(w-w0) / (sqrteps+max(max(abs(w0)))))); 320 | dh = max(max(abs(h-h0) / (sqrteps+max(max(abs(h0)))))); 321 | delta = max(dw,dh); 322 | 323 | % Check for convergence 324 | if j>1 325 | if delta <= tolx 326 | break; 327 | elseif dnorm0-dnorm <= tolfun*max(1,dnorm0) 328 | break; 329 | elseif j==maxiter 330 | break 331 | end 332 | end 333 | 334 | if dispnum>2 % 'iter' 335 | if usePool 336 | fprintf(dispfmt,labindx,repnum,j,dnorm,delta); 337 | else 338 | fprintf(dispfmt,repnum,j,dnorm,delta); 339 | end 340 | end 341 | 342 | % Remember previous iteration results 343 | dnorm0 = dnorm; 344 | w0 = w; 345 | h0 = h; 346 | end 347 | 348 | if dispnum>1 % 'final' or 'iter' 349 | if usePool 350 | fprintf(dispfmt,labindx,repnum,j,dnorm,delta); 351 | else 352 | fprintf(dispfmt,repnum,j,dnorm,delta); 353 | end 354 | end 355 | 356 | end 357 | 358 | % --------------------------- 359 | function checkmatrices(a,w,h,k) 360 | % check for non-negative matrices of the proper size 361 | 362 | if ~ismatrix(a) || ~isnumeric(a) || ~isreal(a) || any(any(~isfinite(a))) 363 | error(message('stats:nnmf:BadA')) 364 | end 365 | [n,m] = size(a); 366 | if ~isempty(w) 367 | if ~ismatrix(w) || ~isnumeric(w)|| ~isreal(w) || any(any(w<0)) || any(any(~isfinite(w))) 368 | error(message('stats:nnmf:BadWNegativeValues')) 369 | elseif ~isequal(size(w),[n k]) 370 | error(message('stats:nnmf:BadWSizeIsWrong', sprintf( '%d', n ), sprintf( '%d', k ))); 371 | end 372 | end 373 | if ~isempty(h) 374 | if ~ismatrix(h) || ~isnumeric(h)|| ~isreal(h) || any(any(h<0)) || any(any(~isfinite(h))) 375 | error(message('stats:nnmf:BadHNegativeValues')) 376 | elseif ~isequal(size(h),[k m]) 377 | error(message('stats:nnmf:BadHSizeIsWrong', sprintf( '%d', k ), sprintf( '%d', m ))); 378 | end 379 | end 380 | end % checkmatrices 381 | 382 | -------------------------------------------------------------------------------- /scripts/+utils/run_flame.m: -------------------------------------------------------------------------------- 1 | function [cope,varcope,tstats] = run_flame( data, vars, design_matrix, contrasts ) 2 | %%function [tstat, thresh] = run_glm( data, contrasts ) 3 | % 4 | % 5 | 6 | % Preallocate arrays 7 | cope = zeros(size(contrasts,1),size(data,2)); 8 | varcope = zeros(size(contrasts,1),size(data,2)); 9 | tstats = zeros(size(contrasts,1),size(data,2)); 10 | 11 | % Main loop 12 | for ii = 1:size(data,2) 13 | 14 | % fit GLM with varcopes as random effects variables in error term 15 | [b, beta, covgam]=flame1(data(:,ii),design_matrix,vars(:,ii)); 16 | cope(:,ii) = contrasts*b; 17 | varcope(:,ii) = sqrt(contrasts*covgam*contrasts'); 18 | 19 | end 20 | 21 | 22 | % Create gaussian variance smoothing function 23 | gaussdist = @(x,mu,sd) 1/(2*pi*sd)*exp(-(x-mu).^2/(2*sd^2)); 24 | variance_smoothing = gaussdist((1:size(data,2))',size(data,2)/2,25); 25 | % Normalise so that area under Gaussian is 1 26 | variance_smoothing = variance_smoothing ./ sum(variance_smoothing); 27 | 28 | for ii = 1:size(cope,1) 29 | tstats(ii,:) = cope(ii,:) ./ fftconv(varcope(ii,:),variance_smoothing); 30 | end -------------------------------------------------------------------------------- /scripts/+utils/run_glm.m: -------------------------------------------------------------------------------- 1 | function [cope,varcope,tstats] = run_glm( data, design_matrix, contrasts ) 2 | %%function [tstat, thresh] = run_glm( data, contrasts ) 3 | % 4 | % 5 | 6 | % Preallocate arrays 7 | cope = zeros(size(contrasts,1),size(data,2)); 8 | varcope = zeros(size(contrasts,1),size(data,2)); 9 | tstats = zeros(size(contrasts,1),size(data,2)); 10 | 11 | % reject bad_trials containing nans 12 | good_inds = ~isnan(sum(data,2)); 13 | 14 | % Precompute design matrix invs 15 | pinvx = pinv(design_matrix(good_inds,:)); 16 | pinvxtx=pinv(design_matrix(good_inds,:)'*design_matrix(good_inds,:)); 17 | 18 | % Main loop 19 | for ii = 1:size(data,2) 20 | 21 | % Compute per-time point GLM across the good trials 22 | [cope(:,ii),varcope(:,ii)] = glm_fast_for_meg(squeeze(data(good_inds,ii)),... 23 | design_matrix(good_inds,:),... 24 | pinvxtx,pinvx,contrasts'); 25 | end 26 | 27 | 28 | % Create gaussian variance smoothing function 29 | gaussdist = @(x,mu,sd) 1/(2*pi*sd)*exp(-(x-mu).^2/(2*sd^2)); 30 | variance_smoothing = gaussdist((1:size(data,2))',size(data,2)/2,25); 31 | % Normalise so that area under Gaussian is 1 32 | variance_smoothing = variance_smoothing ./ sum(variance_smoothing); 33 | 34 | for ii = 1:size(cope,1) 35 | tstats(ii,:) = cope(ii,:) ./ fftconv(varcope(ii,:),variance_smoothing); 36 | end -------------------------------------------------------------------------------- /scripts/+utils/run_group_glm.m: -------------------------------------------------------------------------------- 1 | function [group_copes, thresh] = run_group_glm( data, ... 2 | first_level_design_matrix,first_level_contrasts,... 3 | group_level_design_matrix,group_level_contrasts,... 4 | nperms, use_tstat,use_flame) 5 | %%function [group_copes, thresh] = run_group_glm( data, ... 6 | % first_level_design_matrix,first_level_contrasts,... 7 | % group_level_design_matrix,group_level_contrasts,... 8 | % nperms) 9 | % 10 | % data - [states,time,trials,subjects] 11 | % first_level_design_matrix - [trials, subject predictors,subjects] 12 | % first_level_contrasts - [subject predictors, subject contrasts] 13 | % group_level_design_matrix - [subjects, group predictors] 14 | % group_level_contrasts - [group predictors, group contrasts] 15 | % 16 | 17 | if nargin < 8 || isempty(use_flame) 18 | use_flame=1; 19 | end 20 | 21 | if nargin < 7 || isempty(use_tstat) 22 | use_tstat=0; 23 | end 24 | 25 | if nargin < 6 || isempty(nperms) 26 | nperms = 0; 27 | end 28 | 29 | % Meta info 30 | [nstates,nsamples,ntrials,nsubjs] = size(data); 31 | ncontrasts = size(first_level_contrasts,1); 32 | 33 | % Preallocate data arrays 34 | copes = zeros(nstates,ncontrasts,nsamples,nsubjs); 35 | varcopes = zeros(nstates,ncontrasts,nsamples,nsubjs); 36 | tstats = zeros(nstates,ncontrasts,nsamples,nsubjs); 37 | 38 | group_copes = zeros(nstates,ncontrasts,nsamples); 39 | group_tstats = zeros(nstates,ncontrasts,nsamples); 40 | 41 | % Compute first and group level GLM per state. 42 | fprintf('\nComputing GLM\n'); 43 | for ii = 1:nstates 44 | for jj = 1:nsubjs 45 | [copes(ii,:,:,jj),varcopes(ii,:,:,jj),tstats(ii,:,:,jj)] = utils.run_glm( squeeze(data(ii,:,:,jj))',... 46 | first_level_design_matrix(:,:,jj),first_level_contrasts); 47 | end 48 | 49 | if use_flame==1 50 | % Group level estimate 51 | for jj = 1:ncontrasts 52 | [group_copes(ii,jj,:),~,group_tstats(ii,jj,:)] = utils.run_flame( squeeze(copes(ii,jj,:,:))',... 53 | squeeze(varcopes(ii,jj,:,:))',... 54 | group_level_design_matrix,group_level_contrasts); 55 | end 56 | else 57 | for jj = 1:ncontrasts 58 | [group_copes(ii,jj,:),~,group_tstats(ii,jj,:)] = utils.run_glm( squeeze(copes(ii,jj,:,:))',... 59 | group_level_design_matrix,group_level_contrasts); 60 | end 61 | end 62 | end 63 | 64 | % Compute group level stats with sign-flipping permutations 65 | if nperms > 0 66 | msg_base = 'Computing permutation %d of %d'; 67 | msg = sprintf( msg_base, 1, nperms); 68 | fprintf(msg); 69 | 70 | % Preallocate null distributions and add observed vales 71 | null_dist = zeros(nperms,size(first_level_contrasts,1)); 72 | null_dist(1,:) = squeeze(max(max(group_copes,[],3),[],1)); 73 | 74 | % Run permutations 75 | for ii = 2:nperms 76 | fprintf(repmat('\b',1,length(msg))) 77 | msg = sprintf( msg_base, ii, nperms); 78 | fprintf(msg); 79 | 80 | perm_copes = zeros(nstates,ncontrasts,size(copes,3)); 81 | perms = sign(randn( size(copes,4),1 )); 82 | for jj = 1:nstates% per state 83 | 84 | if use_flame==1 85 | % Group level estimate 86 | for kk = 1:ncontrasts 87 | [perm_copes(jj,kk,:),~,~] = utils.run_flame( squeeze(copes(jj,kk,:,:))',... 88 | squeeze(varcopes(jj,kk,:,:))',... 89 | group_level_design_matrix.*perms,group_level_contrasts); 90 | end 91 | else 92 | for kk = 1:ncontrasts 93 | [perm_copes(jj,kk,:),~,~] = utils.run_glm( squeeze(copes(jj,kk,:,:))',... 94 | group_level_design_matrix.*perms,group_level_contrasts); 95 | end 96 | end 97 | 98 | end 99 | null_dist(ii,:) = squeeze(max(max(perm_copes,[],3),[],1)); 100 | end 101 | 102 | fprintf('\n') 103 | % Estimate threshold 104 | thresh = prctile(null_dist,95,1); 105 | else 106 | thresh = []; 107 | end 108 | -------------------------------------------------------------------------------- /scripts/+utils/run_nnmf.m: -------------------------------------------------------------------------------- 1 | function [nnmf_res,ss] = run_nnmf( S, niterations, summary_plot ) 2 | % function [nnmf_res,ss] = run_nnmf( S, niters, summary_plot ) 3 | % 4 | % 5 | 6 | if nargin < 3 || isempty( summary_plot ) 7 | summary_plot = false; 8 | end 9 | 10 | if nargin < 2 || isempty( niterations ) 11 | niterations = 10; 12 | end 13 | 14 | if summary_plot == true && niterations > 10 15 | warning('Summary plot is likely to be crowded with more than 10 iterations!'); 16 | end 17 | 18 | S.do_plots = 0; 19 | 20 | % Preallocate for SumSquare of residuls 21 | ncomps = S.maxP; 22 | nsamples = size( S.psds,3 ); 23 | ss = zeros( niterations, ncomps); 24 | 25 | % Specify fit function, a unimodal gaussian 26 | gauss_func = @(x,f) f.a1.*exp(-((x-f.b1)/f.c1).^2); 27 | 28 | % Default fit options 29 | options = fitoptions('gauss1'); 30 | 31 | % constrain lower and upper bounds 32 | options.Lower = [0,1,0]; 33 | options.Upper = [Inf,nsamples,nsamples]; 34 | 35 | % Main loop 36 | winning_value = Inf; 37 | if summary_plot == true 38 | specs = zeros( ncomps, nsamples, niterations); 39 | end 40 | 41 | for ii = 1:niterations 42 | 43 | next_nnmf = teh_spectral_nnmf( S ); 44 | 45 | for jj = 1:ncomps 46 | f = fit( linspace(1,nsamples,nsamples)',next_nnmf.nnmf_coh_specs(jj,:)', 'gauss1',options); 47 | resid = next_nnmf.nnmf_coh_specs(jj,:) - gauss_func(1:nsamples,f); 48 | ss(ii,jj) = sum( resid.^2 ); 49 | end 50 | 51 | if sum(ss(ii,:)) < winning_value 52 | nnmf_res = next_nnmf; 53 | winning_value = sum(ss(ii,:)); 54 | end 55 | 56 | if summary_plot == true 57 | specs(:,:,ii) = next_nnmf.nnmf_coh_specs; 58 | end 59 | 60 | end 61 | 62 | 63 | if summary_plot 64 | nrows = ceil( niterations/5 ); 65 | winning_ind = find(winning_value == sum(ss,2)); 66 | 67 | figure('Position',[100 100 1536 768]) 68 | for ii = 1:niterations 69 | subplot( nrows,5, ii); 70 | plot( specs(:,:,ii)','linewidth',2);grid on; 71 | title_text = [ num2str(ii) '- SS: ' num2str(sum(ss(ii,:)))]; 72 | if ii == winning_ind 73 | title_text = [title_text ' - WINNER']; 74 | end 75 | title(title_text,'FontSize',14); 76 | end 77 | 78 | figure 79 | x_vect = 1:niterations; 80 | h = bar(x_vect, sum(ss,2) ); 81 | grid on;hold on 82 | bar(x_vect(winning_ind),sum(ss(winning_ind,:)),'r') 83 | xlabel('Iteration') 84 | ylabel('Residual Sum Squares') 85 | set(gca,'FontSize',14); 86 | 87 | end 88 | 89 | -------------------------------------------------------------------------------- /scripts/+utils/set1_cols.m: -------------------------------------------------------------------------------- 1 | function set1 = set1_cols 2 | 3 | % Colourscheme based on the ColorBrewer Set 1 4 | 5 | % http://colorbrewer2.org/#type=qualitative&scheme=Set1&n=8 6 | set1 = { [228,26,28], [55,126,184], [77,175,74], [152,78,163],... 7 | [255,127,0], [247,129,191], [255,255,51],[166,86,40] }; 8 | 9 | % normalise and convert back to cell array 10 | set1 = cellfun(@(x) x./255, set1,'UniformOutput', false ); -------------------------------------------------------------------------------- /scripts/+utils/set_redblue_colourmap.m: -------------------------------------------------------------------------------- 1 | function cm = set_redblue_colourmap( ax, clmin, clmax, N ) 2 | %%function cm = set_redblue_colourmap( ax, clmin, clmax, N ) 3 | % 4 | % 5 | 6 | if nargin < 4 || isempty(N) 7 | N = 64; 8 | end 9 | 10 | % scale to sensible order of magnitude 11 | min_scale = floor(log10(abs(clmin))); 12 | clmin = clmin*10.^-min_scale; 13 | 14 | max_scale = floor(log10(abs(clmax))); 15 | clmax = clmax*10.^-max_scale; 16 | 17 | % Round up to nearest 1 18 | clmin = -round(abs(clmin),1); 19 | clmax = round(abs(clmax),1); 20 | 21 | % make largest order of magnitude the ones unit 22 | if abs(max_scale) < abs(min_scale) 23 | clmin = clmin ./ 10*(max_scale-min_scale); 24 | elseif abs(max_scale) > abs(min_scale) 25 | clmax = clmax ./ 10*(min_scale-max_scale); 26 | end 27 | 28 | % get positive values 29 | max_ratio = abs(clmax)/ ( abs(clmin) + abs(clmax) ); 30 | max_lims = round(N*max_ratio); 31 | c1 = cat(1,linspace(.5,1,max_lims),linspace(0,1,max_lims),linspace(0,1,max_lims)); 32 | 33 | % get negative values 34 | min_ratio = abs(clmin)/( abs(clmax)+abs(clmin) ); 35 | min_lims = round(N*min_ratio); 36 | c2 = cat(1,linspace(1,0,min_lims),linspace(1,0,min_lims),linspace(1,.5,min_lims)); 37 | 38 | % set map 39 | cm = flipud(cat(2,c1,c2)'); 40 | colormap(ax,cm); 41 | if abs(max_scale) < abs(min_scale) 42 | caxis(ax,[clmin.*10^(1+min_scale) clmax.*10^max_scale]); 43 | elseif abs(max_scale) > abs(min_scale) 44 | caxis(ax,[clmin.*10^min_scale clmax.*10^(1+max_scale)]); 45 | else 46 | caxis(ax,[clmin.*10^min_scale clmax.*10^(max_scale)]); 47 | end -------------------------------------------------------------------------------- /scripts/hmm_0_initialise.m: -------------------------------------------------------------------------------- 1 | % specify the location of this download in the line below: 2 | % eg download_path = '/home/disk3/ajquinn/HMM_frontiers_download'; 3 | download_path = ''; % <- change this line 4 | 5 | if isempty(download_path) 6 | error('Please specify the location of the download path inside hmm_0_initialise.m'); 7 | end 8 | 9 | if exist( download_path, 'dir' ) == 0 10 | error('Download path in hmm_0_initialise.m not found: %s', download_path); 11 | end 12 | 13 | % Add study utilities 14 | addpath( fullfile( download_path , 'scripts' ) ); 15 | 16 | % Get study paths 17 | config = utils.get_studydetails; 18 | 19 | % Initialise OSL 20 | addpath( fullfile( config.scriptdir,'toolboxes','osl','osl-core') ); 21 | osl_startup 22 | 23 | % Add netlab and fmt 24 | addpath( fullfile(osldir,'ohba-external','netlab3.3','netlab') ); 25 | addpath( fullfile(osldir,'ohba-external','fmt') ); 26 | 27 | % Add HMM-MAR to path 28 | addpath(genpath( fullfile( config.scriptdir,'toolboxes','HMM-MAR-master') )); 29 | 30 | % Add distibutionplot to path 31 | addpath(genpath( fullfile( config.scriptdir,'toolboxes','distributionPlot') )); 32 | -------------------------------------------------------------------------------- /scripts/hmm_1_preprocessing.m: -------------------------------------------------------------------------------- 1 | %% Overview 2 | % 3 | % Our preprocessing will take place within a specified analysis directory and 4 | % will process data using three sub-directories. 5 | % raw_data - containing the fif files (after Maxfilter SSS has been applied) 6 | % spm_sss - contains a the raw data imported into an SPM object with minimal processing (artefact channel labelling and coregistration) 7 | % spm_sss_processed - contains the spm objects with the preprocessing pipeline applied. 8 | % 9 | % We begin with SSS fif files from downloaded from 10 | % https://openfmri.org/dataset/ds000117/ these should be present in within the 11 | % raw_data subfolder in the main analysis directory. The folder hierarchy from 12 | % the openfmri.org download should be preserved. 13 | % 14 | % raw_data 15 | % |--> sub001 16 | % |--> run_01_sss.fif 17 | % |--> run_02_sss.fif 18 | % |--> run_03_sss.fif 19 | % |--> run_04_sss.fif 20 | % |--> run_05_sss.fif 21 | % |--> run_06_sss.fif 22 | % |--> highres001.nii 23 | % |--> sub002 24 | % |--> run_01_sss.fif 25 | % |--> run_02_sss.fif 26 | % |--> run_03_sss.fif 27 | % |--> run_04_sss.fif 28 | % |--> run_05_sss.fif 29 | % |--> run_06_sss.fif 30 | % |--> highres001.nii 31 | % | 32 | % |--> sub019 33 | % |--> run_01_sss.fif 34 | % |--> run_02_sss.fif 35 | % |--> run_03_sss.fif 36 | % |--> run_04_sss.fif 37 | % |--> run_05_sss.fif 38 | % |--> run_06_sss.fif 39 | % |--> highres001.nii 40 | % 41 | % These files will be converted into SPM12 format into the spm_sss directory 42 | % before computing the coregistration. All SPM files are copied into the top 43 | % level of spm_sss and named to reflect the relevant subject and session 44 | % numbers. 45 | % 46 | % spm_sss 47 | % |--> spm_sub01_run01.mat 48 | % |--> spm_sub01_run01.dat 49 | % |--> spm_sub01_run02.mat 50 | % |--> spm_sub01_run02.dat 51 | % | 52 | % |--> spm_sub19_run06.mat 53 | % |--> spm_sub19_run06.dat 54 | % 55 | % Finally the spm_sss_processed folder contains the files during and after MEEG 56 | % data preprocessing. These files are copied from spm_sss with the same naming 57 | % conventions. These files will be the inputs to the HMM data preperation 58 | % scripts. 59 | % 60 | % The script will check for pre-existing files before running any 61 | % preprocessing to avoid repeating stages, only missing data files will be 62 | % processed. 63 | 64 | %check raw data is in place 65 | missing_runs = utils.check_raw_data; 66 | 67 | %% IMPORT RAW FIFS WITH SSS 68 | % Here, we read in the raw fif object and conver the data into SPM12 format and 69 | % save a copy in our spm_sss directory. 70 | 71 | indir = fullfile( config.datadir, 'raw_data' ); 72 | outdir = fullfile( config.analysisdir, 'spm_sss' ); 73 | 74 | if ~exist( outdir ) 75 | mkdir(outdir); 76 | end 77 | 78 | 79 | subjects = 1:19; 80 | sessions = 1:6; 81 | 82 | % loop through sessions and subjects 83 | for j = 1:length(subjects) 84 | for k = 1:length(sessions) 85 | if exist(getfullpath(fullfile(outdir,sprintf('spm_sub%02d_run_%02d.mat',subjects(j),sessions(k)))),'file') 86 | % We don't need to copy existing files 87 | fprintf('Already imported spm_sub%02d_run_%02d.mat\n',subjects(j),sessions(k)); 88 | else 89 | % Run the conversion to SPM12 format 90 | fif_in = fullfile(indir,sprintf('sub%03d/run_%02d_sss.fif',subjects(j),sessions(k))); 91 | D = osl_import(fif_in,'outfile',getfullpath(fullfile(outdir,sprintf('spm_sub%02d_run_%02d',subjects(j),sessions(k))))); 92 | end 93 | end 94 | end 95 | 96 | %% Perform coregistration 97 | % Two processing steps are carried out in spm_sss. Firstly, we relabel the 98 | % relevant artefact (ECG/EOG) channels for later use and secondly, we run the 99 | % coregistration. Neither of these steps interact with the MEEG data themselves 100 | % so we keep them as a separate stage. 101 | % 102 | 103 | % If true, |use_existing| will prevent rerunning the coregstration on any files which already contain a coregistration 104 | use_existing = true; 105 | subjects = 1:19; 106 | sessions = 1:6; 107 | 108 | % Main loop through subjects and sessions 109 | for j = 1:length(subjects) 110 | for k = 1:length(sessions) 111 | 112 | % Load data in from spm_sss, note that any changes in this loop are saved into the same file. 113 | D = spm_eeg_load(getfullpath(fullfile(outdir,sprintf('spm_sub%02d_run_%02d',subjects(j),sessions(k))))); 114 | 115 | % Next we re-label the artefact channels 116 | D = D.chantype(find(strcmp(D.chanlabels,'EEG062')),'EOG'); 117 | D = D.chanlabels(find(strcmp(D.chanlabels,'EEG062')),'VEOG'); 118 | 119 | D = D.chantype(find(strcmp(D.chanlabels,'EEG061')),'EOG'); 120 | D = D.chanlabels(find(strcmp(D.chanlabels,'EEG061')),'HEOG'); 121 | 122 | D = D.chantype(find(strcmp(D.chanlabels,'EEG063')),'ECG'); 123 | D = D.chanlabels(find(strcmp(D.chanlabels,'EEG063')),'ECG'); 124 | 125 | D.save(); 126 | 127 | % Skip coreg if needed 128 | if use_existing && isfield(D,'inv') 129 | fprintf('Already coregistered %s\n',D.fname); 130 | continue 131 | end 132 | 133 | % Coregistration is carried out using a call to osl_headmodel. This 134 | % function takes a struct detailing how the coregistration should be 135 | % carried out. Importantly, we specify a D object from spm_sss and a 136 | % strutrual MRI scan from raw_data. 137 | % Note that useheadshape is set to false, typically we would run this 138 | % stage with useheadshape set to true. As the MRI scans included in the 139 | % download have been defaced to ensure participant anonymity the 140 | % headshape points on the face and nose can cause rotational errors in 141 | % the coreg. To avoid this we do not include the headshape points and 142 | % rely only on the fiducials for the coregistration. 143 | coreg_settings = struct; 144 | coreg_settings.D = D.fullfile; 145 | coreg_settings.mri = sprintf('%sraw_data/sub%03d/highres001.nii',config.datadir,subjects(j)) 146 | coreg_settings.useheadshape = false; 147 | coreg_settings.forward_meg = 'Single Shell'; 148 | coreg_settings.use_rhino = true; 149 | coreg_settings.fid.label.nasion='Nasion'; 150 | coreg_settings.fid.label.lpa='LPA'; 151 | coreg_settings.fid.label.rpa='RPA'; 152 | D = osl_headmodel(coreg_settings); 153 | 154 | % Next we generate and save a summary image so we can check each 155 | % coregistration has been carried out sucessfully. 156 | h = report.coreg(D); 157 | report.save_figs(h,outdir,D.fname); 158 | close(h); 159 | 160 | end 161 | end 162 | 163 | %% MEEG Data Preprocessing 164 | % 165 | % All the processing including the electrophysiological data is carried out 166 | % within this loop. Here we apply a range of data preprocessing steps to remove 167 | % artefacts from our data, project the data into source-space and apply a 168 | % parcellation. 169 | % 170 | % Downsampling - Downsample from 1000Hz to 250Hz 171 | % Filtering - We apply a single passband filter to isolate data between 1 and 45Hz 172 | % Bad Segment Detection - An automatic algorithm is used to identify noisy data segments which are removed from subsequent analysis 173 | % Independant Components Analysis - ICA is used to identify artefactual componets by correlation with the EOG and ECG, these are removed from subsequent analysis 174 | % Sensor Normalisation - The Magnetometers and Gradiometers within each dataset are normalised to make their variances comparable. 175 | % Beamforming - An LCMV Beamformer is used to project the sensor data into an 8mm grid in source space 176 | % Parcellation - The source space data is reduced into a network of parcels defined by a NIFTI file 177 | % 178 | % The analysis is carried out on files copied across from spm_sss. Where 179 | % possible, the processsing is carried out on the files in spm_sss_processed 180 | % 'in place', meaning that they do not generate a new SPM object. The 181 | % downsampling and filtering overwrite the old data file, but the ICA, 182 | % beamforming and parcellation are applied by adding online montages to the SPM 183 | % object. 184 | 185 | % Intialise parcellation and study objects 186 | p = parcellation( 'fmri_d100_parcellation_with_PCC_tighterMay15_v2_8mm' ); 187 | s = study( fullfile(config.analysisdir,'spm_sss'),0); 188 | 189 | % Set output and working directories 190 | outdir = fullfile( config.analysisdir,'spm_sss_processed' ); 191 | wd = getfullpath('tempdir'); 192 | mkdir(wd) 193 | if ~exist( outdir ) 194 | mkdir(outdir); 195 | end 196 | 197 | % Preallocate array for ICA sessions that might need manual checking 198 | sessions_to_check = []; 199 | 200 | % Main loop through files within the study object 201 | for j = 1:s.n 202 | 203 | % We don't need to repeat files which are already preprocessed 204 | if exist(fullfile(outdir,s.fnames{j}),'file') 205 | fprintf('Done: %s\n',fullfile(outdir,s.fnames{j})) 206 | continue 207 | else 208 | fprintf('Todo: %s\n',fullfile(outdir,s.fnames{j})) 209 | end 210 | 211 | % Load in MEEG data as an SPM object 212 | D = s.read(j); 213 | 214 | % Downsample and copy, or just copy 215 | if D.fsample > 250 216 | D = spm_eeg_downsample(struct('D',D,'fsample_new',250,'prefix',[wd '/'])); % Note - downsampling cannot be done in-place using prefix='', it just fails 217 | else 218 | D = D.copy(getfullpath(fullfile(pwd,wd,D.fname))); % Copy into working directory 219 | end 220 | 221 | % Apply a 1-45Hz passband filter 222 | D = osl_filter(D,[1 45],'prefix',''); 223 | 224 | % Apply automatric bad segment detection 225 | D = osl_detect_artefacts(D,'badchannels',false); 226 | 227 | % Run ICA artefact detection. This will automatically reject components 228 | % which have correlations larger than .5 with either of the artefact 229 | % channels. 230 | D = osl_africa(D,'used_maxfilter',true); 231 | 232 | % Though the automatic correlations generally work well, we should be 233 | % careful to check for unsusual datasets and possibly manually correct the 234 | % automatic assessment. This is particularly important for relatively noisy 235 | % data or when analysing a new dataset for the first time. 236 | % 237 | % Here we will manally inspect the ICA component rejections for any dataset meeting the following criteria 238 | % # More than 4 ICs rejected 239 | % # Zero ICs rejected 240 | % # No component rejected due to correlation with EOG 241 | % # No component rejected due to correlation with ECG 242 | artefact_chan_corr_thresh = .5; 243 | if isempty(D.ica.bad_components) || length(D.ica.bad_components) > 4 244 | disp('%s components rejected, recommend checking session', length(D.ica.bad_components)); 245 | sessions_to_check = cat(1,sessions_to_check,j); 246 | elseif max(D.ica.metrics.corr_chan_367_EOG.value) < artefact_chan_corr_thresh || ... 247 | max(D.ica.metrics.corr_chan_368_EOG.value) < artefact_chan_corr_thresh 248 | disp('no candidate components for either EOG, recommend checking session'); 249 | sessions_to_check = cat(1,sessions_to_check,j); 250 | elseif max(D.ica.metrics.corr_chan_369_ECG.value) < artefact_chan_corr_thresh 251 | disp('no candidate components for ECG, recommend checking session'); 252 | sessions_to_check = cat(1,sessions_to_check,j); 253 | end 254 | 255 | % This is where manual Africa would go 256 | % D = D.montage('remove',1:D.montage('getnumber')); 257 | % D = osl_africa(D,'do_ident','manual'); 258 | 259 | % Normalise sensor types 260 | S = []; 261 | S.D = D; 262 | S.modalities = {'MEGMAG','MEGPLANAR'}; 263 | S.do_plots = 0; 264 | S.samples2use = good_samples(D,D.indchantype(S.modalities,'GOOD')); 265 | S.trials = 1; 266 | S.pca_dim = 99; 267 | S.force_pca_dim = 0; 268 | S.normalise_method = 'min_eig'; 269 | D = normalise_sensor_data( S ); 270 | 271 | % Run LCMV Beamformer 272 | D = osl_inverse_model(D,p.template_coordinates,'pca_order',50); 273 | 274 | % Do parcellation 275 | D = ROInets.get_node_tcs(D,p.parcelflag,'spatialBasis','Giles'); 276 | 277 | % Save out 278 | D = D.montage('switch',0); 279 | D.copy(fullfile(outdir,s.fnames{j})); 280 | 281 | end 282 | 283 | 284 | %% Epoch information 285 | % 286 | % Finally, we extract the epoch information from the continuous SPM12 files. We 287 | % do not apply the epoching here as the HMM will be run on the continuous data, 288 | % without knowledge of any task structure. The epoch definitions here will 289 | % instead be applied to the HMM state time-courses. 290 | 291 | % Preallocate results array 292 | epochinfo = cell(114,1); 293 | 294 | % Load in preprocessed data 295 | s = study( fullfile(config.analysisdir,'spm_sss_processed'),0); 296 | 297 | for j = 1:s.n 298 | 299 | % Load in MEEG data as an SPM object 300 | D = s.read(j); 301 | 302 | S2 = struct; 303 | S2.D = D; 304 | 305 | % We define a wide window to include the participant responses 306 | pretrig = -1000; % epoch start in ms 307 | posttrig = 2000; % epoch end in ms 308 | S2.timewin = [pretrig posttrig]; 309 | 310 | % define the trials we want from the event information 311 | S2.trialdef(1).conditionlabel = 'Famous_first'; 312 | S2.trialdef(1).eventtype = 'STI101_up'; 313 | S2.trialdef(1).eventvalue = [5]; 314 | S2.trialdef(2).conditionlabel = 'Famous_imm'; 315 | S2.trialdef(2).eventtype = 'STI101_up'; 316 | S2.trialdef(2).eventvalue = [6]; 317 | S2.trialdef(3).conditionlabel = 'Famous_last'; 318 | S2.trialdef(3).eventtype = 'STI101_up'; 319 | S2.trialdef(3).eventvalue = [7]; 320 | 321 | S2.trialdef(4).conditionlabel = 'Unfamiliar_first'; 322 | S2.trialdef(4).eventtype = 'STI101_up'; 323 | S2.trialdef(4).eventvalue = [13]; 324 | S2.trialdef(5).conditionlabel = 'Unfamiliar_imm'; 325 | S2.trialdef(5).eventtype = 'STI101_up'; 326 | S2.trialdef(5).eventvalue = [14]; 327 | S2.trialdef(6).conditionlabel = 'Unfamiliar_last'; 328 | S2.trialdef(6).eventtype = 'STI101_up'; 329 | S2.trialdef(6).eventvalue = [15]; 330 | 331 | S2.trialdef(7).conditionlabel = 'Scrambled_first'; 332 | S2.trialdef(7).eventtype = 'STI101_up'; 333 | S2.trialdef(7).eventvalue = [17]; 334 | S2.trialdef(8).conditionlabel = 'Scrambled_imm'; 335 | S2.trialdef(8).eventtype = 'STI101_up'; 336 | S2.trialdef(8).eventvalue = [18]; 337 | S2.trialdef(9).conditionlabel = 'Scrambled_last'; 338 | S2.trialdef(9).eventtype = 'STI101_up'; 339 | S2.trialdef(9).eventvalue = [19]; 340 | 341 | S2.reviewtrials = 0; 342 | S2.save = 0; 343 | S2.epochinfo.padding = 0; 344 | S2.event = D.events; 345 | S2.fsample = D.fsample; 346 | S2.timeonset = D.timeonset; 347 | 348 | [epochinfo{j}.trl, epochinfo{j}.conditionlabels] = spm_eeg_definetrial(S2); 349 | 350 | end 351 | 352 | % Save epoch information for use after HMM inference 353 | save( fullfile(config.analysisdir,'spm_sss_processed','epochinfo.mat'), 'epochinfo', '-v7.3'); 354 | 355 | %% Run a final sanity check to make sure everything is in place 356 | % 357 | 358 | missing_runs = utils.check_preproc_data; 359 | -------------------------------------------------------------------------------- /scripts/hmm_2_envelope_estimation.m: -------------------------------------------------------------------------------- 1 | %% Overview 2 | % 3 | % Here we load in our source parcellated MEG data from the preprocessing stage, 4 | % perform some normalisation and compute an Amplitude-Envelope HMM. 5 | % 6 | 7 | % Load in study deails 8 | config = utils.get_studydetails; 9 | 10 | %% 11 | 12 | % Find preprocessed data files 13 | datapath = fullfile( config.analysisdir,'spm_sss_processed' ); 14 | s = study(datapath,'Giles'); 15 | 16 | % Load in epoch info and initialise parcellation object 17 | load( fullfile( datapath,'epochinfo.mat') ); 18 | p = parcellation('fmri_d100_parcellation_with_PCC_tighterMay15_v2_8mm'); 19 | 20 | % We'll need to collect the data, T and epoch information 21 | data = []; % HMM ready dataset 22 | T = []; % Length of continuous good segments 23 | R = []; % Indices of single run within data 24 | B = cell(s.n,1); % Indices of bad samples per session 25 | trl = cell(s.n,1); % epoch info per segment 26 | runlen = zeros(s.n,1); % Length of run per good segment 27 | 28 | % Preallocate array to store ERF 29 | nsamples = diff(epochinfo{1}.trl(1,1:2)) + 1; 30 | ntrials = 148; 31 | erf = nan(nsamples,p.n_parcels,ntrials,s.n); 32 | 33 | for ind = 1:s.n 34 | 35 | fprintf('Processing %s\n',s.fnames{ind}); 36 | 37 | %------------------------------- 38 | % continuous file 39 | D = s.read(ind); 40 | D_orig = D.montage('switch',0); 41 | 42 | runlen(ind) = size(D,2); 43 | 44 | %------------------------------- 45 | % get power envelope 46 | dat = osl_envelope( D, 'filter', [2 40], 'orthogonalise',true); 47 | 48 | %------------------------------- 49 | % Smooth and normalise 50 | dat = movmean(dat,25,2,'omitnan'); % 100ms smoothing window 51 | for ll = 1:p.n_parcels 52 | dat(ll,:) = ( dat(ll,:) - nanmean(dat(ll,:)) ) ./ nanstd(dat(ll,:)); 53 | end 54 | 55 | %------------------------------- 56 | % Get badsamples 57 | runlen(ind) = size(dat,2); 58 | bs = ~good_samples( D ); 59 | 60 | % find single good samples - bug when we have consecutive bad segments 61 | xx = find(diff(diff(bs)) == 2)+1; 62 | if ~isempty(xx) 63 | bs(xx) = 1; 64 | end 65 | 66 | % store bad samples 67 | B{ind}=find(bs); 68 | 69 | % indices of good samples 70 | good_inds=setdiff(1:runlen(ind),B{ind}); 71 | 72 | % remove bad samples, 73 | % replace with >> a = zeros(44,size(D,2))*nan;a(:,inds) = dat; 74 | 75 | dat = dat(:,good_inds); 76 | 77 | if any(bs) 78 | 79 | t_good = ~bs; 80 | db = find(diff([0; t_good(:); 0])); 81 | onset = db(1:2:end); 82 | offset = db(2:2:end); 83 | t = offset-onset; 84 | 85 | % sanity check 86 | if size(dat,2) ~= sum(t) 87 | disp('Mismatch between Data and T!!'); 88 | end 89 | else 90 | t = size(dat,2); 91 | end 92 | 93 | %-------------------------------- 94 | % Store info 95 | 96 | offset = sum(T); 97 | 98 | R = cat(1,R,[offset+1 offset+size(dat,2)]); 99 | 100 | T = cat(1,T,t); 101 | 102 | data = cat(2,data,dat); 103 | 104 | %-------------------------------- 105 | % Check evoked result 106 | 107 | % get trial info 108 | trl{ind} = epochinfo{ind}.trl; 109 | 110 | % replace bad samples with nans 111 | subj_data = nan(p.n_parcels,size(D,2)); 112 | subj_data(:,good_inds) = data(:,R(ind,1):R(ind,2)); 113 | 114 | for kk = 1:size(trl{ind},1) 115 | start = trl{ind}(kk,1); 116 | stop = trl{ind}(kk,2); 117 | 118 | if stop > size(subj_data,2) || start > size(subj_data,2) 119 | disp('some trials missing'); 120 | continue 121 | else 122 | erf(:,:,kk,ind) = subj_data(:,start:stop)'; 123 | end 124 | end 125 | end 126 | 127 | % Define HMM folder and save HMM-ready data 128 | hmm_folder = fullfile( config.analysisdir, 'envelope_hmm' ); 129 | if ~exist( hmm_folder ) 130 | mkdir( hmm_folder ); 131 | end 132 | outfile = fullfile( hmm_folder, 'envelope_hmm_data' ); 133 | % 134 | save( outfile, 'data', 'R', 'T', 'B', 'runlen', '-v7.3' ); 135 | 136 | %% Check ERF 137 | % 138 | % Here we plot the ERF created above as a final check that the input data 139 | % to the HMM is properly aligned with respect to our triggeres 140 | 141 | time_vect = linspace(-1,2,751) - .032; 142 | figure; 143 | plot(time_vect, nanmean(nanmean(erf,4),3)) 144 | grid on; 145 | xlabel('Time (secs)') 146 | ylabel('Amplitude Envelope') 147 | 148 | %% HMM inference 149 | % 150 | % Here we infer the HMM itself, a detailed description of the HMM-MAR toolbox 151 | % can be found on https://github.com/OHBA-analysis/HMM-MAR/wiki 152 | % 153 | 154 | % Prepare options structure 155 | options = struct(); 156 | options.verbose = 1; 157 | 158 | % These options specify the data and preprocessing that hmmmar might perform. Further options are discussed here 159 | options.onpower = 0; 160 | options.standardise = 0; 161 | options.Fs = 250; 162 | 163 | % Here we specify the HMM parameters 164 | options.K = 6; % The number of states to infer 165 | options.order = 0; % The lag used, this is only relevant when using MAR observations 166 | options.zeromean = 0; % We do want to model the mean, so zeromean is set off 167 | options.covtype = 'full'; % We want to model the full covariance matrix 168 | 169 | % These options specify parameters relevant for the Stochastic inference. They 170 | % may be omitted to run a standard inference, but this will greatly increase 171 | % the memory and CPU demands during processing. A detailed description of the 172 | % Stochastic options and their usage can be found here: 173 | % https://github.com/OHBA-analysis/HMM-MAR/wiki/User-Guide#stochastic 174 | 175 | options.BIGNinitbatch = 15; 176 | options.BIGNbatch = 15; 177 | options.BIGtol = 1e-7; 178 | options.BIGcyc = 500; 179 | options.BIGundertol_tostop = 5; 180 | options.BIGdelay = 5; 181 | options.BIGforgetrate = 0.7; 182 | options.BIGbase_weights = 0.9; 183 | 184 | % The following loop performs the main HMM inference. We start by 185 | % estimating a 6 state HMM as used in the manuscript. 186 | states_to_infer = [6]; 187 | 188 | % Optionally, we can explore a wider range of values for K by looping through 189 | % several values. This can be done by uncommenting the line below. 190 | % Warning: This is likely to be extremely time-consuming to infer! 191 | 192 | %states_to_infer = 2:2:12; % uncomment this line to explore different numbers of states 193 | 194 | % The HMM inference is repeated a number of times and the results based on 195 | % the iteration with the lowest free energy. Note that this can be 196 | % extremely time-consuming for large datasets. For a quick exploration of 197 | % results, nrepeats can be set to a smaller value or even 1. The full inference 198 | % is run over 10 repeats. 199 | nrepeats = 1; 200 | 201 | for kk = states_to_infer 202 | best_freeenergy = nan; 203 | options.K = kk; 204 | 205 | for irep = 1:nrepeats 206 | % Run the HMM, note we only store a subset of the outputs 207 | % more details can be found here: https://github.com/OHBA-analysis/HMM-MAR/wiki/User-Guide#estimation 208 | [hmm_iter, Gamma_iter, ~, vpath_iter, ~, ~, ~, ~, fehist] = hmmmar (data',T',options); 209 | 210 | if isnan(best_freeenergy) || fehist(end) < best_freeenergy 211 | hmm = hmm_iter; 212 | Gamma = Gamma_iter; 213 | vpath = vpath_iter; 214 | end 215 | end 216 | % Save the HMM outputs 217 | hmm_outfile = fullfile( config.analysisdir, 'envelope_hmm', sprintf('envelope_HMM_K%d',options.K)); 218 | save( hmm_outfile ,'hmm','Gamma','vpath','T') 219 | end 220 | -------------------------------------------------------------------------------- /scripts/hmm_3_embedded_estimation.m: -------------------------------------------------------------------------------- 1 | %% Overview 2 | % 3 | % Here we load in our source parcellated MEG data from the preprocessing stage, 4 | % perform some normalisation and compute an Time-Delay-Embedded HMM. 5 | % 6 | 7 | config = utils.get_studydetails; 8 | 9 | %% 10 | 11 | % Find preprocessed data files 12 | datapath = fullfile( config.analysisdir,'spm_sss_processed' ); 13 | s = study(datapath,'Giles'); 14 | 15 | % Load in epoch info and initialise parcellation object 16 | load( fullfile( datapath,'epochinfo.mat') ); 17 | p = parcellation('fmri_d100_parcellation_with_PCC_tighterMay15_v2_8mm'); 18 | 19 | % We'll need to collect the data, T and epoch information 20 | data = []; % HMM ready dataset 21 | T = []; % Length of continuous good segments 22 | R = []; % Indices of single run within data 23 | B = cell(s.n,1); % Indices of bad samples per session 24 | trl = cell(s.n,1); % epoch info per segment 25 | runlen = zeros(s.n,1); % Length of run per good segment 26 | 27 | % Preallocate array to store ERF 28 | nsamples = diff(epochinfo{1}.trl(1,1:2)) + 1; 29 | ntrials = 148; 30 | erf = nan(nsamples,p.n_parcels,ntrials,s.n); 31 | 32 | % main normalisation loop 33 | for ind = 1:s.n 34 | 35 | fprintf('Processing %s\n',s.fnames{ind}); 36 | 37 | 38 | %------------------------------- 39 | % continuous file 40 | D = s.read(ind); 41 | D_orig = D.montage('switch',0); 42 | 43 | runlen(ind) = size(D,2); 44 | 45 | %------------------------------- 46 | % get data and orthogonalise 47 | dat = D(:,:,1); 48 | 49 | dat = ROInets.remove_source_leakage(dat, 'symmetric'); 50 | 51 | %------------------------------- 52 | % Get badsamples 53 | runlen(ind) = size(dat,2); 54 | bs = ~good_samples( D ); 55 | 56 | % find single good samples - bug when we have consecutive bad segments 57 | xx = find(diff(diff(bs)) == 2)+1; 58 | if ~isempty(xx) 59 | bs(xx) = 1; 60 | end 61 | 62 | % store bad samples 63 | B{ind}=find(bs); 64 | 65 | % indices of good samples 66 | good_inds=setdiff(1:runlen(ind),B{ind}); 67 | 68 | % remove bad samples, 69 | % replace with >> a = zeros(44,size(D,2))*nan;a(:,inds) = dat; 70 | 71 | dat = dat(:,good_inds); 72 | 73 | if any(bs) 74 | 75 | t_good = ~bs; 76 | db = find(diff([0; t_good(:); 0])); 77 | onset = db(1:2:end); 78 | offset = db(2:2:end); 79 | t = offset-onset; 80 | 81 | % sanity check 82 | if size(dat,2) ~= sum(t) 83 | disp('Mismatch between Data and T!!'); 84 | end 85 | else 86 | t = size(dat,2); 87 | end 88 | 89 | %-------------------------------- 90 | % Store info 91 | 92 | offset = sum(T); 93 | 94 | R = cat(1,R,[offset+1 offset+size(dat,2)]); 95 | 96 | T = cat(1,T,t); 97 | 98 | data = cat(2,data,dat); 99 | 100 | %-------------------------------- 101 | % Check evoked result 102 | 103 | % get trial info 104 | trl{ind} = epochinfo{ind}.trl; 105 | 106 | % replace bad samples 107 | subj_data = nan(p.n_parcels,size(D,2)); 108 | subj_data(:,good_inds) = data(:,R(ind,1):R(ind,2)); 109 | 110 | for kk = 1:size(trl{ind},1) 111 | start = trl{ind}(kk,1); 112 | stop = trl{ind}(kk,2); 113 | 114 | if stop > size(subj_data,2) || start > size(subj_data,2) 115 | disp('skipping'); 116 | continue 117 | else 118 | erf(:,:,kk,ind) = subj_data(:,start:stop)'; 119 | end 120 | end 121 | end 122 | 123 | %% 124 | 125 | % Define HMM folder and save HMM-ready data 126 | hmm_folder = fullfile( config.analysisdir, 'embedded_hmm' ); 127 | if ~exist( hmm_folder ) 128 | mkdir( hmm_folder ); 129 | end 130 | outfile = fullfile( hmm_folder, 'embedded_hmm_data' ); 131 | 132 | save( outfile, 'data', 'R', 'T', 'B', 'runlen', '-v7.3' ); 133 | 134 | %% Check ERF 135 | % 136 | % Here we plot the ERF created above as a final check that the input data 137 | % to the HMM is properly aligned with respect to our triggeres 138 | 139 | time_vect = linspace(-1,2,751) - .032; 140 | figure; 141 | plot(time_vect, nanmean(nanmean(erf,4),3)) 142 | grid on; 143 | xlabel('Time (secs)') 144 | ylabel('Amplitude Envelope') 145 | 146 | 147 | %% 148 | run_flip = true; 149 | if run_flip 150 | % need to compute erp above 151 | x = squeeze(nanmean(erf,3)); 152 | x = reshape(permute(x,[2 1 3]),39,[]); 153 | T2 = repmat(751,114,1); 154 | 155 | options_sf = struct(); 156 | options_sf.maxlag = 5; 157 | options_sf.noruns = 20; 158 | options_sf.nbatch = 3; 159 | options_sf.verbose = 1; 160 | flips = findflip(x',T2',options_sf); 161 | 162 | T3 = [R(1,2) sum(diff(R(:,2)))]; % Single scan sessions 163 | data = flipdata(data',T3',flips); 164 | 165 | % Define HMM folder and save HMM-ready data 166 | outfile = fullfile(hmm_folder, 'embedded_hmm_data_flipped.mat' ); 167 | 168 | save( outfile, 'data', 'R', 'T', 'B', 'runlen', '-v7.3' ); 169 | end 170 | 171 | 172 | %% HMM inference 173 | % 174 | % Here we infer the HMM itself, a detailed description of the HMM-MAR toolbox 175 | % can be found on https://github.com/OHBA-analysis/HMM-MAR/wiki 176 | % 177 | 178 | load(outfile); 179 | 180 | % Prepare options structure 181 | options = struct(); 182 | options.verbose = 1; 183 | 184 | % These options specify the data and preprocessing that hmmmar might perform. Further options are discussed here 185 | options.onpower = 0; 186 | options.standardise = 0; 187 | options.Fs = 250; 188 | 189 | % Here we specify the HMM parameters 190 | options.K = 6; % The number of states to infer 191 | options.order = 0; % The lag used, this is only relevant when using MAR observations 192 | options.zeromean = 1; % We do not want to model the mean, so zeromean is set on 193 | options.covtype = 'full'; % We want to model the full covariance matrix 194 | options.embeddedlags = -7:7; % 15 lags are used from -7 to 7 195 | options.pca = 39*4; % The PCA dimensionality reduction is 4 times the number of ROIs 196 | 197 | % These options specify parameters relevant for the Stochastic inference. They 198 | % may be omitted to run a standard inference, but this will greatly increase 199 | % the memory and CPU demands during processing. A detailed description of the 200 | % Stochastic options and their usage can be found here: 201 | % https://github.com/OHBA-analysis/HMM-MAR/wiki/User-Guide#stochastic 202 | options.BIGNinitbatch = 15; 203 | options.BIGNbatch = 15; 204 | options.BIGtol = 1e-7; 205 | options.BIGcyc = 500; 206 | options.BIGundertol_tostop = 5; 207 | options.BIGdelay = 5; 208 | options.BIGforgetrate = 0.7; 209 | options.BIGbase_weights = 0.9; 210 | 211 | % The following loop performs the main HMM inference. We start by 212 | % estimating a 6 state HMM as used in the manuscript. 213 | states_to_infer = [6]; 214 | 215 | % Optionally, we can explore a wider range of values for K by looping through 216 | % several values. This can be done by uncommenting the line below. 217 | % Warning: This is likely to be extremely time-consuming to infer! 218 | 219 | %states_to_infer = 2:2:12; % uncomment this line to explore different numbers of states 220 | 221 | % The HMM inference is repeated a number of times and the results based on 222 | % the iteration with the lowest free energy. Note that this can be 223 | % extremely time-consuming for large datasets. For a quick exploration of 224 | % results, nrepeats can be set to a smaller value or even 1. The full inference 225 | % is run over 10 repeats. 226 | nrepeats = 1; 227 | 228 | for kk = states_to_infer 229 | best_freeenergy = nan; 230 | options.K = kk; 231 | 232 | for irep = 1:nrepeats 233 | % Run the HMM, note we only store a subset of the outputs 234 | % more details can be found here: https://github.com/OHBA-analysis/HMM-MAR/wiki/User-Guide#estimation 235 | [hmm_iter, Gamma_iter, ~, vpath_iter, ~, ~, ~, ~, fehist] = hmmmar (data,T',options); 236 | 237 | if isnan(best_freeenergy) || fehist(end) < best_freeenergy 238 | hmm = hmm_iter; 239 | Gamma = Gamma_iter; 240 | vpath = vpath_iter; 241 | end 242 | end 243 | % Save the HMM outputs 244 | hmm_outfile = fullfile( config.analysisdir, 'embedded_hmm', sprintf('embedded_HMM_K%d',options.K)); 245 | save( hmm_outfile ,'hmm','Gamma','vpath','T') 246 | end 247 | 248 | 249 | %% Load 6 state HMM 250 | % The state-wise spectra will be computed for 6 states 251 | 252 | nstates = 6; 253 | hmm_outfile = fullfile( config.analysisdir, 'embedded_hmm', sprintf('embedded_HMM_K%d',nstates) ); 254 | load( hmm_outfile ,'hmm','Gamma','vpath','T') 255 | 256 | %% Statewise-Spectra 257 | % Next we estimate the state-wise multitaper for each subject and each parcel. 258 | 259 | % account for delay embedding in state gammas 260 | pad_options = struct; 261 | pad_options.embeddedlags = -7:7; 262 | Gamma = padGamma(Gamma, T, pad_options); 263 | 264 | if size(Gamma,1) ~= size(data,1) 265 | warning('The size of data and Gamma do not match'); 266 | end 267 | 268 | % These options specify the how the spectra will be computed. Full details can 269 | % be found here: 270 | % https://github.com/OHBA-analysis/HMM-MAR/wiki/User-Guide#spectra 271 | 272 | spec_options = struct(); 273 | spec_options.fpass = [1 40]; 274 | spec_options.p = 0; % no confidence intervals 275 | spec_options.to_do = [1 0]; % no pdc 276 | spec_options.win = 256; 277 | spec_options.embeddedlags = -7:7; 278 | spec_options.Fs = D.Fsample; 279 | 280 | psd = zeros(114,nstates,39,39,39); 281 | coh = zeros(114,nstates,39,39,39); 282 | for ind = 1:114 283 | disp(ind); 284 | subj_data = data(R(ind,1):R(ind,2),:); 285 | 286 | fit = hmmspectramt(subj_data,R(ind,2)-R(ind,1),Gamma(R(ind,1):R(ind,2),:),spec_options); 287 | for jj = 1:6 288 | psd(ind,jj,:,:,:) = fit.state(jj).psd; 289 | coh(ind,jj,:,:,:) = fit.state(jj).coh; 290 | end 291 | clear fit subj_data 292 | end 293 | 294 | % Save the MT outputs 295 | mt_outfile = fullfile( config.analysisdir, 'embedded_hmm', sprintf('embedded_HMM_K%d_spectra',options.K)); 296 | save( mt_outfile ,'psd','coh') 297 | 298 | -------------------------------------------------------------------------------- /scripts/hmm_4_envelope_results.m: -------------------------------------------------------------------------------- 1 | %% Overview 2 | % 3 | % Here we summarise the results of the HMM inference and compute the 4 | % task-evoked GLM statistics 5 | 6 | config = utils.get_studydetails; 7 | 8 | % Define colours to use in state plots 9 | set1_cols = utils.set1_cols; 10 | 11 | % Define sample rate 12 | sample_rate = 250; 13 | 14 | %% Load in results from envelope data 15 | 16 | % Meta data 17 | method = 'envelope'; 18 | K = 6; 19 | 20 | % Find HMM directory 21 | base = fullfile( config.analysisdir, 'envelope_hmm'); 22 | mkdir( fullfile( base, 'figures' ) ); 23 | 24 | % Load in HMM results 25 | load( fullfile(base, sprintf('envelope_HMM_K%s.mat',num2str(K))) ); 26 | 27 | % Load in run indices 28 | load( fullfile(base, 'envelope_hmm_data.mat'), 'R','B','runlen' ); 29 | 30 | % Load in epoch info 31 | load( fullfile( config.analysisdir, 'spm_sss_processed','epochinfo.mat' ) ); 32 | 33 | % Create basepath for saving results 34 | savebase = fullfile( config.analysisdir, 'envelope_hmm','figures','envelope_HMM_K6'); 35 | 36 | %% Temporal statistics 37 | % 38 | % Here we compute the global temporal statistics from the Amplitude Envelope 39 | % HMM. These are computed per subject and visualised as violin plots 40 | 41 | scan_T = [R(1,2) diff(R(:,2))']; % Indexing individual scan sessions 42 | subj_T = sum(reshape(scan_T,6,[])); % Indexing individal subjects 43 | 44 | % Compute temporal stats 45 | 46 | % Fractional Occupancy is the proportion of time spent in each state 47 | FO = getFractionalOccupancy( Gamma, subj_T, 2); 48 | % Interval Time is the time between subsequent visits to a state 49 | IT = getStateIntervalTimes( Gamma, subj_T, []); 50 | ITmerged = cellfun(@mean,IT);clear IT 51 | % Life Times (or Dwell Times) is the duration of visits to a state 52 | LT = getStateLifeTimes( Gamma, subj_T, []); 53 | LTmerged = cellfun(@mean,LT); clear LT 54 | 55 | % Make summary figures 56 | fontsize = 18; 57 | 58 | figure;subplot(111); 59 | distributionPlot(FO,'showMM',2,'color',{set1_cols{1:size(FO,2)}}); 60 | set(gca,'YLim',[0 1],'FontSize',fontsize) 61 | title('Fractional Occupancy');xlabel('State');ylabel('Proportion');grid on; 62 | print([savebase '_temporalstats_FO'],'-depsc') 63 | 64 | figure;subplot(111); 65 | distributionPlot(LTmerged ./ sample_rate * 1000,'showMM',2,'color',{set1_cols{1:size(FO,2)}}) 66 | title('Life Times');xlabel('State');ylabel('Time (ms)');grid on; 67 | set(gca,'YLim',[0 300],'FontSize',fontsize,'FontSize',fontsize) 68 | print([savebase '_temporalstats_LT'],'-depsc') 69 | 70 | figure;subplot(111); 71 | distributionPlot(ITmerged ./ sample_rate,'showMM',2,'color',{set1_cols{1:size(FO,2)}}) 72 | title('Interval Times');xlabel('State');ylabel('Time (secs)');grid on 73 | set(gca,'YLim',[0 3],'FontSize',fontsize) 74 | print([savebase '_temporalstats_IT'],'-depsc') 75 | 76 | %% Extract Event Related Field and Event Related Gamma 77 | 78 | % Create time vector and adjust for projector lag 79 | time_vect = linspace(-1,2,751); 80 | time_vect_adj = time_vect - .032; % account for projector lag 81 | 82 | % Apply epoching to posterior probabilities, merge subjects and baseline 83 | % correct 84 | erg = utils.load_epoch_results( Gamma', epochinfo, runlen, B, R); 85 | erg = utils.merge_sessions( erg, repelem(1:19,6) ); 86 | erg_bl = utils.baseline_correct( erg, 225:250 ); 87 | 88 | %% Compute two-level GLM 89 | % 90 | % This cell computes the two level GLM from the baseline corrected task-evoked state probabilities. 91 | 92 | % extract conditions for each subject and session. 93 | conds={'Famous','Unfamiliar','Scrambled'}; 94 | cond_inds = cell(19,3); 95 | for ii = 1:19 96 | for jj = 1:6 97 | session = ((ii-1)*6)+jj; 98 | condlabels = epochinfo{session}.conditionlabels; 99 | for kk = 1:3 100 | cond_inds{ii,kk} = cat(2,cond_inds{ii,kk}, find(~cellfun(@isempty, strfind(condlabels,conds{kk}))) + ((jj-1)*150)); 101 | end 102 | end 103 | end 104 | 105 | % Build first level design matrix for each subject 106 | first_level_design_matrix = zeros(size(erg_bl,3),4,19); 107 | first_level_design_matrix(:,1,:) = 1; % Mean term 108 | for ii = 1:19 109 | first_level_design_matrix(cond_inds{ii,1},2,ii) = 1; % Famous Faces 110 | first_level_design_matrix(cond_inds{ii,2},3,ii) = 1; % Unfamiliar Faces 111 | first_level_design_matrix(cond_inds{ii,3},4,ii) = 1; % Scrambled Faces 112 | 113 | % Remove the mean from non-constant regressors as we have an explicit 114 | % constant regressor and contrast for the mean 115 | for jj = 2:4 116 | first_level_design_matrix(:,jj,ii) = demean(first_level_design_matrix(:,jj,ii)); 117 | end 118 | end 119 | 120 | % Define first level contrasts, these are constant across subjects 121 | first_level_contrasts = zeros(3,4); 122 | first_level_contrasts(1,:) = [1 0 0 0]; % Grand Mean 123 | first_level_contrasts(2,:) = [0 1 1 -2]; % Faces>Non-Faces 124 | first_level_contrasts(3,:) = [0 1 -1 0]; % Famous>Unfamiliar 125 | 126 | % Group level design matrix and contrasts - just the mean of the first levels. 127 | group_level_design_matrix = ones(19,1); 128 | group_level_contrasts = 1; 129 | 130 | % Estimate GLM 131 | [copes,thresh_glm] = utils.run_group_glm( erg_bl(:,250:650,:,:),... 132 | first_level_design_matrix,first_level_contrasts,... 133 | group_level_design_matrix,group_level_contrasts,... 134 | 1000,0,0); 135 | 136 | %% Plot GLM results 137 | 138 | % Grand mean figure 139 | figure;subplot(111);hold on;grid on 140 | t = time_vect_adj(250:650); 141 | 142 | for ii = 1:6 143 | plot(t,squeeze(copes(ii,1,:))','Color',set1_cols{ii},'linewidth',2) 144 | 145 | if sum(copes(ii,1,:)>thresh_glm(1)) > 0 146 | sig_inds = find(squeeze(copes(ii,1,:))>thresh_glm(1)); 147 | t2 = ones(size(t))*nan;t2(sig_inds) = t(sig_inds); 148 | x2 = ones(size(t))*nan;x2(sig_inds) = -.08; 149 | plot(t2,x2,'Color',set1_cols{ii},'linewidth',5); 150 | end 151 | 152 | end 153 | 154 | plot([0 0],ylim,'k--') 155 | annotation('textbox',... 156 | [.16 .89 .1 .03],... 157 | 'String','Stimulus Onset',... 158 | 'FontSize',12,... 159 | 'FontWeight','bold',... 160 | 'EdgeColor',[1 1 1],... 161 | 'LineWidth',3,... 162 | 'BackgroundColor',[1 1 1],... 163 | 'Color',[0 0 0]); 164 | plot([.932 .932],ylim,'k--') 165 | annotation('textbox',... 166 | [.61 .89 .2 .03],... 167 | 'String','Average Reaction Time',... 168 | 'FontSize',12,... 169 | 'FontWeight','bold',... 170 | 'EdgeColor',[1 1 1],... 171 | 'LineWidth',3,... 172 | 'BackgroundColor',[1 1 1],... 173 | 'Color',[0 0 0]); 174 | xlim([time_vect_adj(250) time_vect_adj(650)]) 175 | set(gca,'XTick',0:.2:1.5,'FontSize',18) 176 | ylim([-.2 .2]) 177 | xlabel('Time (seconds)') 178 | title('Grand Average') 179 | ylabel('Task Evoked Occupancy') 180 | print([savebase '_FOglm_mean'],'-depsc') 181 | 182 | % Faces>Scrambled Faces figure 183 | figure; 184 | subplot(111);hold on;grid on 185 | t = time_vect_adj(250:650); 186 | 187 | for ii = 1:6 188 | plot(t,squeeze(copes(ii,2,:))','Color',set1_cols{ii},'linewidth',2) 189 | 190 | if sum(copes(ii,2,:)>thresh_glm(2)) > 0 191 | sig_inds = find(squeeze(copes(ii,2,:))>thresh_glm(2)); 192 | t2 = ones(size(t))*nan;t2(sig_inds) = t(sig_inds); 193 | x2 = ones(size(t))*nan;x2(sig_inds) = -.08; 194 | plot(t2,x2,'Color',set1_cols{ii},'linewidth',5); 195 | end 196 | 197 | end 198 | ylim([-.1 .125]) 199 | 200 | plot([0 0],ylim,'k--') 201 | annotation('textbox',... 202 | [.16 .89 .1 .03],... 203 | 'String','Stimulus Onset',... 204 | 'FontSize',12,... 205 | 'FontWeight','bold',... 206 | 'EdgeColor',[1 1 1],... 207 | 'LineWidth',3,... 208 | 'BackgroundColor',[1 1 1],... 209 | 'Color',[0 0 0]); 210 | plot([.932 .932],ylim,'k--') 211 | annotation('textbox',... 212 | [.61 .89 .2 .03],... 213 | 'String','Average Reaction Time',... 214 | 'FontSize',12,... 215 | 'FontWeight','bold',... 216 | 'EdgeColor',[1 1 1],... 217 | 'LineWidth',3,... 218 | 'BackgroundColor',[1 1 1],... 219 | 'Color',[0 0 0]); 220 | xlim([time_vect_adj(250) time_vect_adj(650)]) 221 | set(gca,'XTick',0:.2:1.5,'FontSize',18) 222 | xlabel('Time (seconds)') 223 | ylabel('Task Evoked Occupancy') 224 | title('Faces > Scrambled Faces'); 225 | print([savebase '_FOglm_face'],'-depsc') 226 | 227 | % Famous>Unfamiliar Figure 228 | figure; 229 | subplot(111);hold on;grid on 230 | t = time_vect_adj(250:650); 231 | 232 | for ii = 1:6 233 | plot(t,squeeze(copes(ii,3,:))','Color',set1_cols{ii},'linewidth',2) 234 | 235 | if sum(copes(ii,3,:)>thresh_glm(3)) > 0 236 | sig_inds = find(squeeze(copes(ii,3,:))>thresh_glm(3)); 237 | t2 = ones(size(t))*nan;t2(sig_inds) = t(sig_inds); 238 | x2 = ones(size(t))*nan;x2(sig_inds) = -.08; 239 | plot(t2,x2,'Color',set1_cols{ii},'linewidth',5); 240 | end 241 | 242 | end 243 | ylim([-.1 .125]) 244 | 245 | plot([0 0],ylim,'k--') 246 | annotation('textbox',... 247 | [.16 .89 .1 .03],... 248 | 'String','Stimulus Onset',... 249 | 'FontSize',12,... 250 | 'FontWeight','bold',... 251 | 'EdgeColor',[1 1 1],... 252 | 'LineWidth',3,... 253 | 'BackgroundColor',[1 1 1],... 254 | 'Color',[0 0 0]); 255 | plot([.932 .932],ylim,'k--') 256 | annotation('textbox',... 257 | [.61 .89 .2 .03],... 258 | 'String','Average Reaction Time',... 259 | 'FontSize',12,... 260 | 'FontWeight','bold',... 261 | 'EdgeColor',[1 1 1],... 262 | 'LineWidth',3,... 263 | 'BackgroundColor',[1 1 1],... 264 | 'Color',[0 0 0]); 265 | xlim([time_vect_adj(250) time_vect_adj(650)]) 266 | set(gca,'XTick',0:.2:1.5,'FontSize',18) 267 | xlabel('Time (seconds)') 268 | ylabel('Task Evoked Occupancy') 269 | title('Famous > Unfamiliar') 270 | print([savebase '_FOglm_famous'],'-depsc') 271 | 272 | %% Mean Activation Maps 273 | % 274 | % For the envelope HMM, we take the summary of each state directly from the 275 | % observtion models stored in hmm.state, this provides the information for both 276 | % the mean and functional connectivity results. 277 | 278 | % load parcellation 279 | parc = parcellation('fmri_d100_parcellation_with_PCC_tighterMay15_v2_8mm'); 280 | 281 | % Activation maps are normalised within each state to allow for simple visualisation of the states topology 282 | net_mean = zeros(39,size(Gamma,2)); 283 | for k = 1:size(Gamma,2) 284 | 285 | net_mean(:,k) = zscore( diag(hmm.state(k).Omega.Gam_rate) ./ hmm.state(k).Omega.Gam_shape ); 286 | 287 | end 288 | 289 | % visualise state in OSLEYES 290 | parc.osleyes(net_mean); 291 | 292 | % Optionally save a nifti of the results, these are used to generate the 293 | % figures in the paper via HCP Workbench 294 | parc.savenii( net_mean, [savebase '_meanactivations']); 295 | 296 | 297 | %% Node Weight Maps 298 | 299 | % As with the mean activation maps, the node weights are normalised to aid visualisation 300 | net_nw = zeros(39,size(Gamma,2)); 301 | thresh_mean = zeros(size(Gamma,2),1); 302 | for k = 1:size(Gamma,2) 303 | 304 | G = hmm.state(k).Omega.Gam_rate ./ hmm.state(k).Omega.Gam_shape; 305 | G = G - diag(diag(G)); 306 | nw = sum(G,1)' + sum(G,2); 307 | net_nw(:,k) = zscore(nw); 308 | 309 | end 310 | 311 | % visualise state in OSLEYES 312 | parc.osleyes(net_nw); 313 | 314 | % Optionally save a nifti of the results 315 | parc.savenii( net_nw, [savebase '_nodeweights']); 316 | -------------------------------------------------------------------------------- /scripts/hmm_5_embedded_results.m: -------------------------------------------------------------------------------- 1 | %% Overview 2 | % 3 | % Here we summarise the results of the HMM inference and compute the 4 | % task-evoked GLM statistics 5 | 6 | config = utils.get_studydetails; 7 | 8 | % Define colours to use in state plots 9 | set1_cols = utils.set1_cols; 10 | 11 | % Define sample rate 12 | sample_rate = 250; 13 | 14 | %% Load in results from envelope data 15 | 16 | % Meta data 17 | method = 'embedded'; 18 | K = 6; 19 | 20 | % Find HMM directory 21 | base = fullfile( config.analysisdir, sprintf('%s_hmm',method)); 22 | mkdir( fullfile( base, 'figures' ) ); 23 | 24 | % Load in HMM results 25 | load( fullfile(base, sprintf('%s_HMM_K%s.mat',method,num2str(K))) ); 26 | 27 | % Load in run indices 28 | load( fullfile(base, sprintf('%s_hmm_data_flipped.mat',method)), 'R','B','runlen' ); 29 | 30 | % Load in epoch info 31 | load( fullfile( config.analysisdir, 'spm_sss_processed','epochinfo.mat' ) ); 32 | 33 | % Create basepath for saving results 34 | savebase = fullfile( config.analysisdir, sprintf('%s_hmm',method),'figures',sprintf('%s_HMM_K6',method)); 35 | if ~exist( savebase ) 36 | mkdir(savebase); 37 | end 38 | 39 | % account for delay embedding in state gammas 40 | pad_options.embeddedlags = -7:7; 41 | Gamma = padGamma(Gamma, T, pad_options); 42 | 43 | %% Temporal statistics 44 | % 45 | % Here we compute the global temporal statistics from the Time-Delay-Embedded 46 | % HMM. These are computed per subject and visualised as violin plots 47 | 48 | scan_T = [R(1,2) diff(R(:,2))']; % Indexing individual scan sessions 49 | subj_T = sum(reshape(scan_T,6,[])); % Indexing individal subjects 50 | 51 | % Compute temporal stats 52 | 53 | % Fractional Occupancy is the proportion of time spent in each state 54 | FO = getFractionalOccupancy( Gamma, subj_T, 2); 55 | % Interval Time is the time between subsequent visits to a state 56 | IT = getStateIntervalTimes( Gamma, subj_T, []); 57 | ITmerged = cellfun(@mean,IT);clear IT 58 | % Life Times (or Dwell Times) is the duration of visits to a state 59 | LT = getStateLifeTimes( Gamma, subj_T, []); 60 | LTmerged = cellfun(@mean,LT); clear LT 61 | 62 | 63 | % Plot temporal stats 64 | fontsize = 18; 65 | 66 | figure;subplot(111); 67 | distributionPlot(FO,'showMM',2,'color',{set1_cols{1:size(FO,2)}}); 68 | set(gca,'YLim',[0 1],'FontSize',fontsize) 69 | title('Fractional Occupancy');xlabel('State');ylabel('Proportion');grid on; 70 | print([savebase '_temporalstats_FO'],'-depsc') 71 | 72 | figure;subplot(111); 73 | distributionPlot(LTmerged ./ sample_rate * 1000,'showMM',2,'color',{set1_cols{1:size(FO,2)}}) 74 | title('Life Times');xlabel('State');ylabel('Time (ms)');grid on; 75 | set(gca,'YLim',[0 300],'FontSize',fontsize,'FontSize',fontsize) 76 | print([savebase '_temporalstats_LT'],'-depsc') 77 | 78 | figure;subplot(111); 79 | distributionPlot(ITmerged ./ sample_rate,'showMM',2,'color',{set1_cols{1:size(FO,2)}}) 80 | title('Interval Times');xlabel('State');ylabel('Time (secs)');grid on 81 | set(gca,'YLim',[0 3],'FontSize',fontsize) 82 | print([savebase '_temporalstats_IT'],'-depsc') 83 | 84 | %% Extract Event Related Field and Event Related Gamma 85 | 86 | % Create time vector and adjust for projector lag 87 | time_vect = linspace(-1,2,751); 88 | time_vect_adj = time_vect - .032; % account for projector lag 89 | 90 | % Apply epoching to posterior probabilities, merge subjects and baseline 91 | % correct 92 | erg = utils.load_epoch_results( Gamma', epochinfo, runlen, B, R); 93 | erg = utils.merge_sessions( erg, repelem(1:19,6) ); 94 | erg_bl = utils.baseline_correct( erg, 225:250 ); 95 | 96 | %% Compute two-level GLM 97 | 98 | % extract conditions for each subject and session. 99 | conds={'Famous','Unfamiliar','Scrambled'}; 100 | cond_inds = cell(19,3); 101 | for ii = 1:19 102 | for jj = 1:6 103 | session = ((ii-1)*6)+jj; 104 | condlabels = epochinfo{session}.conditionlabels; 105 | for kk = 1:3 106 | cond_inds{ii,kk} = cat(2,cond_inds{ii,kk}, find(~cellfun(@isempty, strfind(condlabels,conds{kk}))) + ((jj-1)*150)); 107 | end 108 | end 109 | end 110 | 111 | % Build first level design matrix 112 | first_level_design_matrix = zeros(size(erg_bl,3),4,19); 113 | first_level_design_matrix(:,1,:) = 1; % Mean term 114 | for ii = 1:19 115 | first_level_design_matrix(cond_inds{ii,1},2,ii) = 1; % Famous Faces 116 | first_level_design_matrix(cond_inds{ii,2},3,ii) = 1; % Unfamiliar Faces 117 | first_level_design_matrix(cond_inds{ii,3},4,ii) = 1; % Scrambled Faces 118 | 119 | % Remove the mean from non-constant regressors as we have an explicit 120 | % constant regressor and contrast for the mean 121 | for jj = 2:4 122 | first_level_design_matrix(:,jj,ii) = demean(first_level_design_matrix(:,jj,ii)); 123 | end 124 | end 125 | 126 | % Define first level contrasts 127 | first_level_contrasts = zeros(3,4); 128 | first_level_contrasts(1,:) = [1 0 0 0]; % Grand Mean 129 | first_level_contrasts(2,:) = [0 1 1 -2]; % Faces>Non-Faces 130 | first_level_contrasts(3,:) = [0 1 -1 0]; % Famous>Unfamiliar 131 | 132 | % Group level design matrix and contrasts - just the mean of the first 133 | % levels 134 | group_level_design_matrix = ones(19,1); 135 | group_level_contrasts = 1; 136 | 137 | % Estimate GLM 138 | [copes,thresh_glm] = utils.run_group_glm( erg_bl(:,250:650,:,:),... 139 | first_level_design_matrix,first_level_contrasts,... 140 | group_level_design_matrix,group_level_contrasts,... 141 | 1000); 142 | 143 | %% Plot GLM results 144 | 145 | % Grand mean figure 146 | figure;subplot(111);hold on;grid on 147 | t = time_vect_adj(250:650); 148 | 149 | for ii = 1:6 150 | plot(t,squeeze(copes(ii,1,:))','Color',set1_cols{ii},'linewidth',2) 151 | 152 | if sum(copes(ii,1,:)>thresh_glm(1)) > 0 153 | sig_inds = find(squeeze(copes(ii,1,:))>thresh_glm(1)); 154 | t2 = ones(size(t))*nan;t2(sig_inds) = t(sig_inds); 155 | x2 = ones(size(t))*nan;x2(sig_inds) = -.08; 156 | plot(t2,x2,'Color',set1_cols{ii},'linewidth',5); 157 | end 158 | 159 | end 160 | 161 | plot([0 0],ylim,'k--') 162 | annotation('textbox',... 163 | [.16 .89 .1 .03],... 164 | 'String','Stimulus Onset',... 165 | 'FontSize',12,... 166 | 'FontWeight','bold',... 167 | 'EdgeColor',[1 1 1],... 168 | 'LineWidth',3,... 169 | 'BackgroundColor',[1 1 1],... 170 | 'Color',[0 0 0]); 171 | plot([.932 .932],ylim,'k--') 172 | annotation('textbox',... 173 | [.61 .89 .2 .03],... 174 | 'String','Average Reaction Time',... 175 | 'FontSize',12,... 176 | 'FontWeight','bold',... 177 | 'EdgeColor',[1 1 1],... 178 | 'LineWidth',3,... 179 | 'BackgroundColor',[1 1 1],... 180 | 'Color',[0 0 0]); 181 | xlim([time_vect_adj(250) time_vect_adj(650)]) 182 | set(gca,'XTick',0:.2:1.5,'FontSize',18) 183 | ylim([-.2 .2]) 184 | xlabel('Time (seconds)') 185 | title('Grand Average') 186 | ylabel('Task Evoked Occupancy') 187 | print([savebase '_FOglm_mean'],'-depsc') 188 | 189 | % Faces>Scrambled Faces figure 190 | figure; 191 | subplot(111);hold on;grid on 192 | t = time_vect_adj(250:650); 193 | 194 | for ii = 1:6 195 | plot(t,squeeze(copes(ii,2,:))','Color',set1_cols{ii},'linewidth',2) 196 | 197 | if sum(copes(ii,2,:)>thresh_glm(2)) > 0 198 | sig_inds = find(squeeze(copes(ii,2,:))>thresh_glm(2)); 199 | t2 = ones(size(t))*nan;t2(sig_inds) = t(sig_inds); 200 | x2 = ones(size(t))*nan;x2(sig_inds) = -.08; 201 | plot(t2,x2,'Color',set1_cols{ii},'linewidth',5); 202 | end 203 | 204 | end 205 | ylim([-.1 .125]) 206 | 207 | plot([0 0],ylim,'k--') 208 | annotation('textbox',... 209 | [.16 .89 .1 .03],... 210 | 'String','Stimulus Onset',... 211 | 'FontSize',12,... 212 | 'FontWeight','bold',... 213 | 'EdgeColor',[1 1 1],... 214 | 'LineWidth',3,... 215 | 'BackgroundColor',[1 1 1],... 216 | 'Color',[0 0 0]); 217 | plot([.932 .932],ylim,'k--') 218 | annotation('textbox',... 219 | [.61 .89 .2 .03],... 220 | 'String','Average Reaction Time',... 221 | 'FontSize',12,... 222 | 'FontWeight','bold',... 223 | 'EdgeColor',[1 1 1],... 224 | 'LineWidth',3,... 225 | 'BackgroundColor',[1 1 1],... 226 | 'Color',[0 0 0]); 227 | xlim([time_vect_adj(250) time_vect_adj(650)]) 228 | set(gca,'XTick',0:.2:1.5,'FontSize',18) 229 | xlabel('Time (seconds)') 230 | ylabel('Task Evoked Occupancy') 231 | title('Faces > Scrambled Faces'); 232 | print([savebase '_FOglm_face'],'-depsc') 233 | 234 | % Famous>Unfamiliar Figure 235 | figure; 236 | subplot(111);hold on;grid on 237 | t = time_vect_adj(250:650); 238 | 239 | for ii = 1:6 240 | plot(t,squeeze(copes(ii,3,:))','Color',set1_cols{ii},'linewidth',2) 241 | 242 | if sum(copes(ii,3,:)>thresh_glm(3)) > 0 243 | sig_inds = find(squeeze(copes(ii,3,:))>thresh_glm(3)); 244 | t2 = ones(size(t))*nan;t2(sig_inds) = t(sig_inds); 245 | x2 = ones(size(t))*nan;x2(sig_inds) = -.08; 246 | plot(t2,x2,'Color',set1_cols{ii},'linewidth',5); 247 | end 248 | 249 | end 250 | ylim([-.1 .125]) 251 | 252 | plot([0 0],ylim,'k--') 253 | annotation('textbox',... 254 | [.16 .89 .1 .03],... 255 | 'String','Stimulus Onset',... 256 | 'FontSize',12,... 257 | 'FontWeight','bold',... 258 | 'EdgeColor',[1 1 1],... 259 | 'LineWidth',3,... 260 | 'BackgroundColor',[1 1 1],... 261 | 'Color',[0 0 0]); 262 | plot([.932 .932],ylim,'k--') 263 | annotation('textbox',... 264 | [.61 .89 .2 .03],... 265 | 'String','Average Reaction Time',... 266 | 'FontSize',12,... 267 | 'FontWeight','bold',... 268 | 'EdgeColor',[1 1 1],... 269 | 'LineWidth',3,... 270 | 'BackgroundColor',[1 1 1],... 271 | 'Color',[0 0 0]); 272 | xlim([time_vect_adj(250) time_vect_adj(650)]) 273 | set(gca,'XTick',0:.2:1.5,'FontSize',18) 274 | xlabel('Time (seconds)') 275 | ylabel('Task Evoked Occupancy') 276 | title('Famous > Unfamiliar') 277 | print([savebase '_FOglm_famous'],'-depsc') 278 | 279 | %% State descriptions 280 | % 281 | % The state descriptions for the embedded HMM are taken from the state-wise 282 | % multitaper estimation (in contrast to the envelope, utils.re we made direct use of 283 | % the observation model). 284 | % 285 | % To aid visualisation, we compute two Non-Negative Matrix Factorisations so we 286 | % can avoid setting arbitrary frequency bands. This does not change the results 287 | % of the HMM, but simply provides a data-driven method for splitting the 288 | % continuous spectrum into a set of peaks. 289 | 290 | parc = parcellation('fmri_d100_parcellation_with_PCC_tighterMay15_v2_8mm'); 291 | 292 | %% Broadband power plots 293 | mt_outfile = fullfile( config.analysisdir, 'embedded_hmm', sprintf('embedded_HMM_K%d_spectra',K)); 294 | load( mt_outfile ) 295 | 296 | net_mean = zeros(39,size(psd,2)); 297 | for kk = 1:size(psd,2) 298 | tmp = squeeze( mean(mean(abs(psd(:,kk,1:29,:,:)),3),1) ); 299 | net_mean(:,kk) = zscore(diag(tmp)); 300 | end 301 | 302 | % visualise state in OSLEYES 303 | parc.osleyes(net_mean); 304 | 305 | % Optionally save a nifti of the results, these are used to generate the 306 | % figures in the paper via HCP Workbench 307 | parc.savenii( net_mean, [savebase '_meanactivations_broadband']); 308 | 309 | %% Broadband glass brain networks 310 | 311 | % Compute GMM Threshold 312 | H = []; 313 | for kk = 1:6 314 | G = squeeze( mean(mean(abs(psd(:,kk,1:29,:,:)),3),1) ); 315 | G = G-diag(diag(G)); 316 | H = [H, reshape(G(G~=0),1,[])]; 317 | end 318 | 319 | S2 = struct; 320 | S2.do_fischer_xform = false; 321 | S2.do_plots = true; 322 | S2.data = H; 323 | [ graphgmm_res ] = teh_graph_gmm_fit( S2 ); 324 | 325 | % Plot glass brain networks 326 | for kk = 1:6 327 | 328 | G = squeeze( mean(mean(abs(psd(:,kk,1:29,:,:)),3),1) ); 329 | G(G0 331 | 332 | h = parc.plot_network( G ); 333 | h.Parent.Parent.Position(3) = 420; 334 | h.Parent.FontSize = 18; 335 | view([0 90]); 336 | zoom(1); 337 | 338 | end 339 | end 340 | 341 | %% Spectral Mode NNMF 342 | % 343 | 344 | % Compute the NNMF 345 | mt_outfile = fullfile( config.analysisdir, 'embedded_hmm', 'embedded_HMM_K6_spectra'); 346 | load( mt_outfile, 'psd' ); 347 | 348 | S = []; 349 | S.psds = psd(:,:,1:29,:,:); 350 | S.maxP=4; 351 | S.maxPcoh=4; 352 | S.do_plots = true; 353 | nnmf_res = nnmf_res = utils.run_nnmf( S, 20 ); 354 | 355 | nnmf_outfile = fullfile( config.analysisdir, 'embedded_hmm', 'embedded_HMM_K6_nnmf'); 356 | save(nnmf_outfile,'nnmf_res') 357 | 358 | % Visualise the mode shapes 359 | for ii = 1:3 360 | figure('Position',[100 100*ii 256 256]) 361 | h = area(nnmf_res.nnmf_coh_specs(ii,:)); 362 | h.FaceAlpha = .5; 363 | h.FaceColor = [.5 .5 .5]; 364 | grid on;axis('tight'); 365 | set(gca,'YTickLabel',[],'FontSize',14); 366 | end 367 | 368 | 369 | %% Spectral Mode Power Plots 370 | nnmf_outfile = fullfile( config.analysisdir, 'embedded_hmm', 'embedded_HMM_K6_nnmf'); 371 | load( nnmf_outfile ); 372 | 373 | net_mean = zeros(39,4,size(Gamma,2)); 374 | thresh_mean = zeros(size(Gamma,2),1); 375 | for k = 1:size(Gamma,2) 376 | net_mean(:,:,k) = squeeze(nnmf_res.nnmf_psd_maps(k,:,:))'; 377 | end 378 | 379 | for k = 1:size(Gamma,2) 380 | dat = net_mean(:,1:3,k); 381 | parc.osleyes( dat ) 382 | end 383 | 384 | 385 | %% Spectral Mode Network Plots 386 | 387 | for kk = 1:6 388 | 389 | G = squeeze(sum(nnmf_res.nnmf_coh_maps(kk,3,:,:),2)); 390 | 391 | S2 = struct; 392 | S2.do_fischer_xform = false; 393 | S2.do_plots = false; 394 | S2.data = nnmf_res.nnmf_coh_maps(kk,1:3,:,:); 395 | S2.data = S2.data(S2.data~=0); 396 | [ graphgmm_res ] = teh_graph_gmm_fit( S2 ); 397 | 398 | for jj = 1:3 399 | G = squeeze(nnmf_res.nnmf_coh_maps(kk,jj,:,:)); 400 | G(G 0 403 | h = parc.plot_network(G); 404 | h.Parent.Parent.Position(3) = 420; 405 | h.Parent.FontSize = 18; 406 | set(gca,'CLim',[graphgmm_res.orig_th max(S2.data)]); 407 | view([0 90]); 408 | zoom(1); 409 | else 410 | disp('No connections survived thresholding'); 411 | end 412 | 413 | end 414 | end 415 | 416 | 417 | %% HMM regularised TF plots 418 | % 419 | % Finally we compute the HMM regularised TF plots utils.ch show the induced power 420 | % responses as modelled by the HMM for a given parcel. 421 | 422 | % nodes of interest 423 | node = 9; % this can be changed to select another node from the parcellation 424 | 425 | tf_hmm = zeros(K,39,751,19); 426 | 427 | % Generate HMM Regularised TF Response 428 | for ii = 1:19 429 | for jj = 1:6 430 | tf_hmm(jj,:,:,ii) = (abs(squeeze(psd(ii,jj,:,node,node)))*squeeze(nanmean(erg_bl(jj,:,:,ii),3))) .* FO(ii,jj); 431 | end 432 | end 433 | 434 | % Make a plot summarising the TF response, task-evoked gammas and state-wise spectra 435 | freq_vect = 1:39; 436 | figure('Position',[100 100 768 768]) 437 | % Spectra 438 | ax(1) = axes('Position',[.1 .3 .2 .6]);hold on 439 | for ii = 1:6 440 | plot(ax(1),abs(squeeze(nanmean(psd(:,ii,:,node,node),1))),freq_vect,... 441 | 'linewidth',2,'Color',set1_cols{ii}); 442 | end 443 | set(ax(1),'Xdir','reverse','XTickLabel',[]);grid on 444 | ylabel('Frequency (Hz)') 445 | 446 | % ERG 447 | ax(2) = axes('Position',[.3 .1 .6 .2]);hold on 448 | dat = nanmean(nanmean(erg_bl,4),3);% - repmat(erg_baselines,1,751); 449 | for ii = 1:6 450 | plot(ax(2), time_vect_adj,dat(ii,:),'linewidth',2,'Color',set1_cols{ii}) 451 | end 452 | axis('tight');grid on 453 | hold on;plot([0 0],ylim,'k--','linewidth',1.5); 454 | plot([.932 .932],ylim,'k--','linewidth',1.5); 455 | xlim([-.1 2]) 456 | xlabel('Time (secs)') 457 | ylabel('Relative Occupancy') 458 | 459 | % Contour plot 460 | ax(3) = axes('Position',[.3 .3 .6 .6]); 461 | dat = squeeze(sum(mean(tf_hmm(:,:,:,:),4),1)); 462 | bc = squeeze(mean(dat(:,225:250),2)); 463 | dat = dat-repmat(bc,1,751); 464 | contourf(ax(3), time_vect_adj,freq_vect,dat,24,'linestyle','none') 465 | grid on 466 | c = colorbar;c.Position(1) = .91; 467 | set(ax(3),'XTickLabel',[],'YTickLabel',[]) 468 | hold on;plot([0 0],ylim,'k--','linewidth',1.5); 469 | plot([.932 .932],ylim,'k--','linewidth',1.5); 470 | xlim([-.1 2]) 471 | utils.set_redblue_colourmap(ax(3),min(min(dat)), max(max(dat))) 472 | 473 | 474 | --------------------------------------------------------------------------------