function [X,Pnames,Index,idx,jdx,kdx]=spm_DesMtx(varargin) % Design matrix construction from factor level and covariate vectors % FORMAT [X,Pnames] = spm_DesMtx( list) % FORMAT [X,Pnames,Index,idx,jdx,kdx] = spm_DesMtx(FCLevels,Constraint,FCnames) % % % - set of arguments specifying a portion of design matrix (see below) % - FCnames parameter, or Constraint and FCnames parameters, are optional % - a list of multiple triples can be % specified, where FCnames or Constraint-FCnames may be omitted % within any triple. The program then works recursively. % % X - design matrix % Pnames - paramater names as (constructed from FCnames) - a cellstr % Index - integer index of factor levels % - only returned when computing a single design matrix partition % % idx,jdx,kdx - reference vectors mapping I & Index (described below) % - only returned when computing a single design matrix partition % for unconstrained factor effects ('-' or '~') % % ---------------- % % FORMAT [nX,nPnames] = spm_DesMtx('sca',X1,Pnames1,X2,Pnames2,...) % Produces a scaled design matrix nX with max(abs(nX(:))<=1, suitable % for imaging with: image((nX+1)*32) % X1,X2,... - Design matrix partitions % Pnames1, Pnames2,... - Corresponding parameter name string mtx/cellstr (opt) % nX - Scaled design matrix % nPnames - Concatenated parameter names for columns of nX % %__________________________________________________________________________ % % Returns design matrix corresponding to given vectors containing % levels of a factor; two way interactions; covariates (n vectors); % ready-made sections of design matrix; and factor by covariate % interactions. % % The specification for the design matrix is passed in sets of arguments, % each set corresponding to a particular Factor/Covariate/&c., specifying % a section of the design matrix. The set of arguments consists of the % FCLevels matrix (Factor/Covariate levels), an optional constraint string, % and an optional (string) name matrix containing the names of the % Factor/Covariate/&c. % % MAIN EFFECTS: For a main effect, or single factor, the FCLevels % matrix is an integer vector whose values represent the levels of the % factor. The integer factor levels need not be positive, nor in % order. In the '~' constraint types (below), a factor level of zero % is ignored (treated as no effect), and no corresponding column of % design matrix is created. Effects for the factor levels are entered % into the design matrix *in increasing order* of the factor levels. % Check Pnames to find out which columns correspond to which levels of % the factor. % % TWO WAY INTERACTIONS: For a two way interaction effect between two % factors, the FCLevels matrix is an nx2 integer matrix whose columns % indicate the levels of the two factors. An effect is included for % each unique combination of the levels of the two factors. Again, % factor levels must be integer, though not necessarily positive. % Zero levels are ignored in the '~' constraint types described below. % % CONSTRAINTS: Each FactorLevels vector/matrix may be followed by an % (optional) ConstraintString. % % ConstraintStrings for main effects are: % '-' - No Constraint % '~' - Ignore zero level of factor % (I.e. cornerPoint constraint on zero level, % (same as '.0', except zero level is always ignored, % (even if factor only has zero level, in which case % (an empty DesMtx results and a warning is given % '+0' - sum-to-zero constraint % '+0m' - Implicit sum-to-zero constraint % '.' - CornerPoint constraint % '.0' - CornerPoint constraint applied to zero factor level % (warns if there is no zero factor level) % Constraints for two way interaction effects are % '-' - No Constraints % '~' - Ignore zero level of any factor % (I.e. cornerPoint constraint on zero level, % (same as '.ij0', except zero levels are always ignored % '+i0','+j0','+ij0' - sum-to-zero constraints % '.i', '.j', '.ij' - CornerPoint constraints % '.i0','.j0','.ij0' - CornerPoint constraints applied to zero factor level % (warns if there is no zero factor level) % '+i0m', '+j0m' - Implicit sum-to-zero constraints % % With the exception of the "ignore zero" '~' constraint, constraints % are only applied if there are sufficient factor levels. CornerPoint % and explicit sum-to-zero Constraints are applied to the last level of % the factor. % % The implicit sum-to-zero constraints "mean correct" appropriate rows % of the relevant design matrix block. For a main effect, constraint % '+0m' "mean corrects" the main effect block across columns, % corresponding to factor effects B_i, where B_i = B'_i - mean(B'_i) : % The B'_i are the fitted parameters, effectively *relative* factor % parameters, relative to their mean. This leads to a rank deficient % design matrix block. If Matlab's pinv, which implements a % Moore-Penrose pseudoinverse, is used to solve the least squares % problem, then the solution with smallest L2 norm is found, which has % mean(B'_i)=0 provided the remainder of the design is unique (design % matrix blocks of full rank). In this case therefore the B_i are % identically the B'_i - the mean correction imposes the constraint. % % % COVARIATES: The FCLevels matrix here is an nxc matrix whose columns % contain the covariate values. An effect is included for each covariate. % Covariates are identified by ConstraintString 'C'. % % % PRE-SPECIFIED DESIGN BLOCKS: ConstraintString 'X' identifies a % ready-made bit of design matrix - the effect is the same as 'C'. % % % FACTOR BY COVARIATE INTERACTIONS: are identified by ConstraintString % 'FxC'. The last column is understood to contain the covariate. Other % columns are taken to contain integer FactorLevels vectors. The % (unconstrained) interaction of the factors is interacted with the % covariate. Zero factor levels are ignored if ConstraintString '~FxC' % is used. % % % NAMES: Each Factor/Covariate can be 'named', by passing a name % string. Pass a string matrix, or cell array (vector) of strings, % with rows (cells) naming the factors/covariates in the respective % columns of the FCLevels matrix. These names default to , , % , &c., and are used in the construction of the Pnames % parameter names. % E.g. for an interaction, spm_DesMtx([F1,F2],'+ij0',['subj';'cond']) % giving parameter names such as subj*cond_{1,2} etc... % % Pnames returns a string matrix whose successive rows describe the % effects parameterised in the corresponding columns of the design % matrix. `Fac1*Fac2_{2,3}' would refer to the parameter for the % interaction of the two factors Fac1 & Fac2, at the 2nd level of the % former and the 3rd level of the latter. Other forms are % - Simple main effect (level 1) : _{1} % - Three way interaction (level 1,2,3) : **_{1,2,3} % - Two way factor interaction by covariate interaction : % : @*_{1,1} % - Column 3 of prespecified DesMtx block (if unnamed) % : [1] % The special characters `_*()[]{}' are recognised by the scaling % function (spm_DesMtx('sca',...), and should therefore be avoided % when naming effects and covariates. % % % INDEX: An Integer Index matrix is returned if only a single block of % design matrix is being computed (single set of parameters). It % indexes the actual order of the effect levels in the design matrix block. % (Factor levels are introduced in order, regardless of order of % appearence in the factor index matrices, so that the parameters % vector has a sensible order.) This is used to aid recursion. % % Similarly idx,jdx & kdx are indexes returned for a single block of % design matrix consisting of unconstrained factor effects ('-' or '~'). % These indexes map I and Index (in a similar fashion to the `unique` % function) as follows: % - idx & jdx are such that I = Index(:,jdx)' and Index = I(idx,:)' % where vector I is given as a column vector % - If the "ignore zeros" constraint '~' is used, then kdx indexes the % non-zero (combinations) of factor levels, such that % I(kdx,:) = Index(:,jdx)' and Index == I(kdx(idx),:)' % % ---------------- % % The design matrix scaling feature is designed to return a scaled % version of a design matrix, with values in [-1,1], suitable for % visualisation. Special care is taken to apply the same normalisation % to blocks of design matrix reflecting a single effect, to preserve % appropriate relationships between columns. Identification of effects % corresponding to columns of design matrix portions is via the parameter % names matrices. The design matrix may be passed in any number of % parts, provided the corresponding parameter names are given. It is % assumed that the block representing an effect is contained within a % single partition. Partitions supplied without corresponding parameter % names are scaled on a column by column basis, the parameters labelled as % in the returned nPnames matrix. % % Effects are identified using the special characters `_*()[]{}' used in % parameter naming as follows: (here ? is a wildcard) % - ?(?) - general block (column normalised) % - ?[?] - specific block (block normalised) % - ?_{?} - main effect or interaction of main effects % - ?@?_{?} - factor by covariate interaction % Blocks are identified by looking for runs of parameters of the same type % with the same names: E.g. a block of main effects for factor 'Fac1' % would have names like Fac1_{?}. % % Scaling is as follows: % * fMRI blocks are scaled around zero to lie in [-1,1] % * No scaling is carried out if max(abs(tX(:))) is in [.4,1] % This protects dummy variables from normalisation, even if % using implicit sum-to-zero constraints. % * If the block has a single value, it's replaced by 1's % * FxC blocks are normalised so the covariate values cover [-1,1] % but leaving zeros as zero. % * Otherwise, block is scaled to cover [-1,1]. % %__________________________________________________________________________ % Copyright (C) 1994-2011 Wellcome Trust Centre for Neuroimaging % Andrew Holmes % $Id: spm_DesMtx.m 5219 2013-01-29 17:07:07Z spm $ %-Parse arguments for recursive construction of design matrices %========================================================================== if ~nargin, error('Insufficient arguments'); end if ischar(varargin{1}) %-Non-recursive action string usage Constraint=varargin{1}; elseif nargin>=2 && ~(ischar(varargin{2}) || iscell(varargin{2})) [X1,Pnames1] = spm_DesMtx(varargin{1}); [X2,Pnames2] = spm_DesMtx(varargin{2:end}); X = [X1,X2]; Pnames = [Pnames1;Pnames2]; return elseif nargin>=3 && ~(ischar(varargin{3}) || iscell(varargin{3})) [X1,Pnames1] = spm_DesMtx(varargin{1:2}); [X2,Pnames2] = spm_DesMtx(varargin{3:end}); X = [X1,X2]; Pnames = [Pnames1;Pnames2]; return elseif nargin>=4 [X1,Pnames1] = spm_DesMtx(varargin{1:3}); [X2,Pnames2] = spm_DesMtx(varargin{4:end}); X = [X1,X2]; Pnames = [Pnames1;Pnames2]; return else %-If I is a vector, make it a column vector I = varargin{1}; if size(I,1)==1, I=I'; end %-Sort out constraint and Factor/Covariate name parameters if nargin<2, Constraint = '-'; else Constraint = varargin{2}; end if isempty(I), Constraint = 'mt'; end if nargin<3, FCnames = {}; else FCnames = varargin{3}; end if char(FCnames), FCnames = cellstr(FCnames); end end switch lower(Constraint), case 'mt' %-Empty I case %========================================================================== X = []; Pnames = {}; Index = []; case {'c','x'} %-Covariate effect, or ready-made design matrix %========================================================================== %-I contains a covariate (C), or is to be inserted "as is" (X) X = I; %-Construct parameter name index %-------------------------------------------------------------------------- if isempty(FCnames) if strcmp(Constraint,'C'), FCnames={''}; else FCnames={''}; end end if length(FCnames)==1 && size(X,2)>1 Pnames = cell(size(X,2),1); for i=1:size(X,2) Pnames{i} = sprintf('%s [%d]',FCnames{1},i); end elseif length(FCnames)~=size(X,2) error('FCnames doesn''t match covariate/X matrix') else Pnames = FCnames; end case {'-(1)','~(1)'} %-Simple main effect ('~' ignores zero levels) %========================================================================== %-Sort out arguments %-------------------------------------------------------------------------- if size(I,2)>1, error('Simple main effect requires vector index'), end if any(I~=floor(I)), error('Non-integer indicator vector'), end if isempty(FCnames), FCnames = {''}; elseif length(FCnames)>1, error('Too many FCnames'), end nXrows = size(I,1); % Sort out unique factor levels - ignore zero level in '~(1)' usage %-------------------------------------------------------------------------- if Constraint(1)~='~' [Index,idx,jdx] = unique(I'); kdx = [1:nXrows]; else [Index,idx,jdx] = unique(I(I~=0)'); kdx = find(I~=0)'; if isempty(Index) X=[]; Pnames={}; Index=[]; warning(['factor has only zero level - ',... 'returning empty DesMtx partition']) return end end %-Set up unconstrained X matrix & construct parameter name index %-------------------------------------------------------------------------- nXcols = length(Index); %-Columns in ascending order of corresponding factor level X = zeros(nXrows,nXcols); Pnames = cell(nXcols,1); for ii=1:nXcols %-ii indexes i in Index X(:,ii) = I==Index(ii); %-Can't use: for i=Index, X(:,i) = I==i; end % in case Index has holes &/or doesn't start at 1! Pnames{ii} = sprintf('%s_{%d}',FCnames{1},Index(ii)); end %-Don't append effect level if only one level if nXcols==1, Pnames=FCnames; end case {'-','~'} %-Main effect / interaction ('~' ignores zero levels) %========================================================================== if size(I,2)==1 %-Main effect - process directly [X,Pnames,Index,idx,jdx,kdx] = spm_DesMtx(I,[Constraint,'(1)'],FCnames); return end if any((I(:))~=floor(I(:))), error('Non-integer indicator vector'), end % Sort out unique factor level combinations & build design matrix %-------------------------------------------------------------------------- %-Make "raw" index to unique effects nI = I - ones(size(I,1),1)*min(I); tmp = max(I)-min(I)+1; tmp = [fliplr(cumprod(tmp(end:-1:2))),1]; rIndex = sum(nI.*(ones(size(I,1),1)*tmp),2)+1; %-Ignore combinations where any factor has level zero in '~' usage if Constraint(1)=='~' rIndex(any(I==0,2))=0; if all(rIndex==0) X=[]; Pnames={}; Index=[]; warning(['no non-zero factor level combinations - ',... 'returning empty DesMtx partition']) return end end %-Build design matrix based on unique factor combinations [X,Pnames,sIndex,idx,jdx,kdx]=spm_DesMtx(rIndex,[Constraint,'(1)']); %-Sort out Index matrix Index = I(kdx(idx),:)'; %-Construct parameter name index %-------------------------------------------------------------------------- if isempty(FCnames) tmp = ['',sprintf('*',2:size(I,2))]; elseif length(FCnames)==size(I,2) tmp = [FCnames{1},sprintf('*%s',FCnames{2:end})]; else error('#FCnames mismatches #Factors in interaction') end Pnames = cell(size(Index,2),1); for c = 1:size(Index,2) Pnames{c} = ... [sprintf('%s_{%d',tmp,Index(1,c)),sprintf(',%d',Index(2:end,c)),'}']; end case {'fxc','-fxc','~fxc'} %-Factor dependent covariate effect % ('~' ignores zero factor levels) %========================================================================== %-Check %-------------------------------------------------------------------------- if size(I,2)==1, error('FxC requires multi-column I'), end F = I(:,1:end-1); C = I(:,end); if ~all(all(F==floor(F),1),2) error('non-integer indicies in F partition of FxC'), end if isempty(FCnames) Fnames = ''; Cnames = ''; elseif length(FCnames)==size(I,2) Fnames = FCnames(1:end-1); Cnames = FCnames{end}; else error('#FCnames mismatches #Factors+#Cov in FxC') end %-Set up design matrix X & names matrix - ignore zero levels if '~FxC' use %-------------------------------------------------------------------------- if Constraint(1)~='~', [X,Pnames,Index] = spm_DesMtx(F,'-',Fnames); else [X,Pnames,Index] = spm_DesMtx(F,'~',Fnames); end X = X.*(C*ones(1,size(X,2))); Pnames = cellstr([repmat([Cnames,'@'],size(Index,2),1),char(Pnames)]); case {'.','.0','+0','+0m'} %-Constrained simple main effect %========================================================================== if size(I,2)~=1, error('Simple main effect requires vector index'), end [X,Pnames,Index] = spm_DesMtx(I,'-(1)',FCnames); %-Impose constraint if more than one effect %-------------------------------------------------------------------------- %-Apply uniqueness constraints ('.' & '+0') to last effect, which is % in last column, since column i corresponds to level Index(i) %-'.0' corner point constraint is applied to zero factor level only nXcols = size(X,2); zCol = find(Index==0); if nXcols==1 && ~strcmp(Constraint,'.0') error('only one level: can''t constrain') elseif strcmp(Constraint,'.') X(:,nXcols)=[]; Pnames(nXcols)=[]; Index(nXcols)=[]; elseif strcmp(Constraint,'.0') zCol = find(Index==0); if isempty(zCol), warning('no zero level to constrain') elseif nXcols==1, error('only one level: can''t constrain'), end X(:,zCol)=[]; Pnames(zCol)=[]; Index(zCol)=[]; elseif strcmp(Constraint,'+0') X(find(X(:,nXcols)),:)=-1; X(:,nXcols)=[]; Pnames(nXcols)=[]; Index(nXcols)=[]; elseif strcmp(Constraint,'+0m') X = X - 1/nXcols; end case {'.i','.i0','.j','.j0','.ij','.ij0','+i0','+j0','+ij0','+i0m','+j0m'} %-Two way interaction effects %========================================================================== if size(I,2)~=2, error('Two way interaction requires Nx2 index'), end [X,Pnames,Index] = spm_DesMtx(I,'-',FCnames); %-Implicit sum to zero %-------------------------------------------------------------------------- if any(strcmp(Constraint,{'+i0m','+j0m'})) SumIToZero = strcmp(Constraint,'+i0m'); SumJToZero = strcmp(Constraint,'+j0m'); if SumIToZero %-impose implicit SumIToZero constraints Js = sort(Index(2,:)); Js = Js([1,1+find(diff(Js))]); for j = Js rows = find(I(:,2)==j); cols = find(Index(2,:)==j); if length(cols)==1 error('Only one level: Can''t constrain') end X(rows,cols) = X(rows,cols) - 1/length(cols); end end if SumJToZero %-impose implicit SumJToZero constraints Is = sort(Index(1,:)); Is = Is([1,1+find(diff(Is))]); for i = Is rows = find(I(:,1)==i); cols = find(Index(1,:)==i); if length(cols)==1 error('Only one level: Can''t constrain') end X(rows,cols) = X(rows,cols) - 1/length(cols); end end %-Explicit sum to zero %-------------------------------------------------------------------------- elseif any(strcmp(Constraint,{'+i0','+j0','+ij0'})) SumIToZero = any(strcmp(Constraint,{'+i0','+ij0'})); SumJToZero = any(strcmp(Constraint,{'+j0','+ij0'})); if SumIToZero %-impose explicit SumIToZero constraints i = max(Index(1,:)); if i==min(Index(1,:)) error('Only one i level: Can''t constrain'), end cols = find(Index(1,:)==i); %-columns to delete for c=cols j=Index(2,c); t_cols=find(Index(2,:)==j); t_rows=find(X(:,c)); %-This ij equals -sum(ij) over other i % (j fixed for this col c). %-So subtract weight of this ij factor from % weights for all other ij factors for this j % to impose the constraint. X(t_rows,t_cols) = X(t_rows,t_cols)... -X(t_rows,c)*ones(1,length(t_cols)); %-( Next line would do it, but only first time round, when all ) % ( weights are 1, and only one weight per row for this j. ) % X(t_rows,t_cols)=-1*ones(length(t_rows),length(t_cols)); end %-delete columns X(:,cols)=[]; Pnames(cols)=[]; Index(:,cols)=[]; end if SumJToZero %-impose explicit SumJToZero constraints j = max(Index(2,:)); if j==min(Index(2,:)) error('Only one j level: Can''t constrain'), end cols=find(Index(2,:)==j); for c=cols i=Index(1,c); t_cols=find(Index(1,:)==i); t_rows=find(X(:,c)); X(t_rows,t_cols) = X(t_rows,t_cols)... -X(t_rows,c)*ones(1,length(t_cols)); end %-delete columns X(:,cols)=[]; Pnames(cols)=[]; Index(:,cols)=[]; end %-Corner point constraints %-------------------------------------------------------------------------- elseif any(strcmp(Constraint,{'.i','.i0','.j','.j0','.ij','.ij0'})) CornerPointI = any(strcmp(Constraint,{'.i','.i0','.ij','.ij0'})); CornerPointJ = any(strcmp(Constraint,{'.j','.j0','.ij','.ij0'})); if CornerPointI %-impose CornerPointI constraints if Constraint(end)~='0', i = max(Index(1,:)); else i = 0; end cols=find(Index(1,:)==i); %-columns to delete if isempty(cols) warning('no zero i level to constrain') elseif all(Index(1,:)==i) error('only one i level: can''t constrain') end %-delete columns X(:,cols)=[]; Pnames(cols)=[]; Index(:,cols)=[]; end if CornerPointJ %-impose CornerPointJ constraints if Constraint(end)~='0', j = max(Index(2,:)); else j = 0; end cols=find(Index(2,:)==j); if isempty(cols) warning('no zero j level to constrain') elseif all(Index(2,:)==j) error('only one j level: can''t constrain') end X(:,cols)=[]; Pnames(cols)=[]; Index(:,cols)=[]; end end case {'sca'} %-Scale DesMtx for visualisation %========================================================================== nX = []; nPnames = {}; Carg = 2; %-Loop through the arguments accumulating scaled design matrix nX %-------------------------------------------------------------------------- while Carg <= nargin rX = varargin{Carg}; Carg=Carg+1; if Carg<=nargin && ~isempty(varargin{Carg}) && ... (ischar(varargin{Carg}) || iscellstr(varargin{Carg})) rPnames = char(varargin{Carg}); Carg=Carg+1; else %-No names to work out blocks from - normalise by column rPnames = repmat('',size(rX,2),1); end %-Pad out rPnames with 20 spaces to permit looking past line ends rPnames = [rPnames,repmat(' ',size(rPnames,1),20)]; while ~isempty(rX) if size(rX,2)>1 && max([1,find(rPnames(1,:)=='(')]) < ... max([0,find(rPnames(1,:)==')')]) %-Non-specific block: find the rest & column normalise round zero %====================================================================== c1 = max(find(rPnames(1,:)=='(')); d = any(diff(abs(rPnames(:,1:c1))),2)... | ~any(rPnames(2:end,c1+1:end)==')',2); t = min(find([d;1])); %-Normalise columns of block around zero %------------------------------------------------------------------ tmp = size(nX,2); nX = [nX, zeros(size(rX,1),t)]; for i=1:t if ~any(rX(:,i)) nX(:,tmp+i) = 0; else nX(:,tmp+i) = rX(:,i)/max(abs(rX(:,i))); end end nPnames = [nPnames; cellstr(rPnames(1:t,:))]; rX(:,1:t) = []; rPnames(1:t,:)=[]; elseif size(rX,2)>1 && max([1,find(rPnames(1,:)=='[')]) < ... max([0,find(rPnames(1,:)==']')]) %-Block: find the rest & normalise together %====================================================================== c1 = max(find(rPnames(1,:)=='[')); d = any(diff(abs(rPnames(:,1:c1))),2)... | ~any(rPnames(2:end,c1+1:end)==']',2); t = min(find([d;1])); %-Normalise block %------------------------------------------------------------------ nX = [nX,sf_tXsca(rX(:,1:t))]; nPnames = [nPnames; cellstr(rPnames(1:t,:))]; rX(:,1:t) = []; rPnames(1:t,:)=[]; elseif size(rX,2)>1 && max([1,strfind(rPnames(1,:),'_{')]) < ... max([0,find(rPnames(1,:)=='}')]) %-Factor, interaction of factors, or FxC: find the rest... %====================================================================== c1 = max(strfind(rPnames(1,:),'_{')); d = any(diff(abs(rPnames(:,1:c1+1))),2)... | ~any(rPnames(2:end,c1+2:end)=='}',2); t = min(find([d;1])); %-Normalise block %------------------------------------------------------------------ tX = rX(:,1:t); if any(rPnames(1,1:c1)=='@') %-FxC interaction C = tX(tX~=0); tX(tX~=0) = 2*(C-min(C))/max(C-min(C))-1; nX = [nX,tX]; else %-Straight interaction nX = [nX,sf_tXsca(tX)]; end nPnames = [nPnames; cellstr(rPnames(1:t,:))]; rX(:,1:t) = []; rPnames(1:t,:)=[]; else %-Dunno! Just column normalise %====================================================================== nX = [nX,sf_tXsca(rX(:,1))]; nPnames = [nPnames; cellstr(rPnames(1,:))]; rX(:,1) = []; rPnames(1,:)=[]; end end end X = nX; Pnames = nPnames; otherwise %-Mis-specified arguments - ERROR %========================================================================== if ischar(varargin{1}) error('unrecognised action string') else error('unrecognised constraint type') end end %========================================================================== % function nX = sf_tXsca(tX) %========================================================================== function nX = sf_tXsca(tX) if nargin==0, nX=[]; return, end if abs(max(abs(tX(:)))-0.7)<(.3+1e-10) nX = tX; elseif all(tX(:)==tX(1)) nX = ones(size(tX)); elseif max(abs(tX(:)))<1e-10 nX = zeros(size(tX)); else nX = 2*(tX-min(tX(:)))/max(tX(:)-min(tX(:)))-1; end