Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/tests-walkthroughs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ jobs:
products: |
Statistics_and_Machine_Learning_Toolbox
Signal_Processing_Toolbox
Image_Processing_Toolbox
cache: true

- name: Run walkthrough tests
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Other top-level classes (not subclasses of `image_vector`):
- **`brainpathway` / `brainpathway_multisubject`** — connectivity / pathway-modeling objects.
- **`canlab_dataset`** — generic subject × variable behavioral/clinical data container with its own `glm`, `mediation`, `scatterplot`, etc.
- **`fmri_glm_design_matrix`**, **`fmri_timeseries`**, **`predictive_model`** — specialized containers for design matrices, raw timeseries, and ML model artifacts.
- **`glm_map`** — scikit-learn-style estimator for mass-univariate GLM / multiple regression. Bundles the design specification (wrapping an `fmri_glm_design_matrix` in `.design`, or a direct `.X` matrix), the fitted result maps (betas, t, contrast estimates/t as `statistic_image` objects), and design diagnostics (VIF, contrast VIF, leverage, collinearity). Workflow is `g = glm_map(...)`, `g = fit(g, fmri_data_obj)`, then `diagnostics`, `table`, `montage`. Supports 1st-level event designs (`build_design`, `import_SPM`) and 2nd-level/group designs; `fmri_data.regress` is the compute engine.

### MATLAB `@class` directories

Expand Down
77 changes: 77 additions & 0 deletions CanlabCore/@glm_map/add_contrasts.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
function obj = add_contrasts(obj, C, names, varargin)
% Add one or more linear contrasts to a glm_map object.
%
% Appends rows of contrast weights (over regressors) to obj.contrasts and
% names to obj.contrast_names, validating that the weight vectors match the
% number of regressors in the design.
%
% :Usage:
% ::
%
% obj = add_contrasts(obj, C, names)
%
% :Inputs:
%
% **obj:**
% A glm_map object with a design available (obj.X non-empty).
%
% **C:**
% A [num_contrasts x num_regressors] matrix; each ROW is one contrast
% over the regressors. (Stored internally as [regressors x contrasts].)
%
% **names:**
% Cell array of contrast names, one per row of C. Optional; defaults
% to 'Con1', 'Con2', ...
%
% :Outputs:
%
% **obj:**
% glm_map with contrasts and contrast_names appended.
%
% :Examples:
% ::
%
% g = glm_map('X', [ones(30,1) zscore((1:30)') randn(30,1)], 'level', 2);
% g = add_contrasts(g, [0 1 0; 0 0 1], {'slope' 'nuisance'});
%
% :See also:
% - diagnostics, fmri_data.regress
%
% ..
% Programmers' notes:
% 2026 - Initial implementation.
% ..

if nargin < 3 || isempty(names)
names = {};
end
if ~iscell(names), names = cellstr(names); end

nreg = obj.num_regressors;
if nreg == 0
error('glm_map:NoDesign', 'No design available; set obj.X or build a design before adding contrasts.');
end

% Each row of C is a contrast; validate width against number of regressors
if size(C, 2) ~= nreg
error('glm_map:ContrastSize', ...
'Each contrast must have %d weights (one per regressor); got %d columns in C.', nreg, size(C, 2));
end

ncon_new = size(C, 1);

% Default names
if isempty(names)
start = numel(obj.contrast_names);
names = arrayfun(@(k) sprintf('Con%d', start + k), 1:ncon_new, 'UniformOutput', false);
elseif numel(names) ~= ncon_new
error('glm_map:ContrastNames', 'Number of names (%d) must match number of contrasts (%d).', numel(names), ncon_new);
end

% Append. Internally contrasts are stored [regressors x contrasts].
obj.contrasts = [obj.contrasts, C'];
obj.contrast_names = [obj.contrast_names(:); names(:)]';

obj.history{end + 1} = sprintf('add_contrasts: added %d contrast(s)', ncon_new);

end % add_contrasts
84 changes: 84 additions & 0 deletions CanlabCore/@glm_map/build_design.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
function obj = build_design(obj, varargin)
% Build the design matrix X for an event/1st-level glm_map from onsets.
%
% Delegates to the wrapped fmri_glm_design_matrix object's build method,
% which convolves onsets/durations with the basis set and assembles the
% design matrix (interest, covariate, and baseline partitions) into
% design.xX.X with column names in design.xX.name. After this call, the
% Dependent obj.X and obj.regressor_names read through to the built design.
%
% :Usage:
% ::
%
% obj = build_design(obj, varargin)
%
% :Inputs:
%
% **obj:**
% A glm_map object with a non-empty .design (fmri_glm_design_matrix)
% that has conditions/onsets assigned.
%
% :Optional Inputs:
%
% **'doplot' / 'plot':**
% Plot the resulting design via fmri_glm_design_matrix.plot (default off).
%
% :Outputs:
%
% **obj:**
% The glm_map object with the wrapped design built (design.xX.X populated).
%
% :Examples:
% ::
%
% TR = 2; nscan = 200;
% ons = {[10 40 70 100]', [25 55 85 115]'}; % 2 conditions, 1 session
% d = fmri_glm_design_matrix(TR, 'nscan', nscan, 'units', 'secs', ...
% 'onsets', ons, 'condition_names', {'A' 'B'});
% g = glm_map(d);
% g = build_design(g);
% size(g.X) % built design matrix
% g.regressor_names
%
% :See also:
% - fmri_glm_design_matrix, fmri_glm_design_matrix.build, onsets2fmridesign
%
% ..
% Programmers' notes:
% 2026 - Initial implementation (delegates to fmri_glm_design_matrix.build).
% ..

% -------------------------------------------------------------------------
% Parse options
% -------------------------------------------------------------------------
doplot = any(strcmpi(varargin, 'doplot')) || any(strcmpi(varargin, 'plot'));

% -------------------------------------------------------------------------
% Validate
% -------------------------------------------------------------------------
if isempty(obj.design)
error('glm_map:NoDesign', ...
'build_design requires an fmri_glm_design_matrix in obj.design (event/1st-level mode).');
end

if isempty(obj.design.Sess) || isempty(obj.design.Sess(1).U) || isempty(obj.design.Sess(1).U(1).name)
error('glm_map:NoOnsets', ...
['The wrapped design has no conditions/onsets assigned. Add them first, e.g. ' ...
'fmri_glm_design_matrix(TR, ''nscan'', nscan, ''onsets'', ons, ''condition_names'', names).']);
end

% -------------------------------------------------------------------------
% Build
% -------------------------------------------------------------------------
obj.design = build(obj.design);

obj.level = 1; % building implies a first-level/event model

if doplot
plot(obj.design);
end

obj.history{end + 1} = sprintf('build_design: built design matrix [%d x %d] from onsets', ...
size(obj.X, 1), size(obj.X, 2));

end % build_design
73 changes: 73 additions & 0 deletions CanlabCore/@glm_map/check_properties.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
function obj = check_properties(obj, varargin)
% Validate and enforce types on a glm_map object's properties.
%
% Lightweight consistency checks: ensures cell-array metadata fields are
% cells, the level is 1 or 2, contrast bookkeeping is consistent, and any
% contained statistic_image maps pass their own check_properties.
%
% :Usage:
% ::
%
% obj = check_properties(obj)
%
% :Inputs:
%
% **obj:**
% A glm_map object.
%
% :Outputs:
%
% **obj:**
% The glm_map object with types/fields coerced where needed. Warns on
% inconsistencies it cannot silently fix.
%
% :See also:
% - statistic_image.check_properties
%
% ..
% Programmers' notes:
% 2026 - Initial implementation.
% ..

% Cell-array fields
if ~iscell(obj.contrast_names), obj.contrast_names = cellstr(obj.contrast_names); end
if ~iscell(obj.warnings), obj.warnings = {}; end
if ~iscell(obj.history), obj.history = {}; end
if ~iscell(obj.regressor_names_direct) && ~isempty(obj.regressor_names_direct)
obj.regressor_names_direct = cellstr(obj.regressor_names_direct);
end

% Level
if ~ismember(obj.level, [1 2])
warning('glm_map:BadLevel', 'level should be 1 or 2; got %s.', num2str(obj.level));
end

% is_timeseries should be logical
obj.is_timeseries = logical(obj.is_timeseries);

% AR only meaningful for timeseries
if obj.level == 2 && obj.is_timeseries
warning('glm_map:LevelTimeseries', 'is_timeseries is true but level is 2 (group); AR models are not appropriate for group images.');
end

% Contrast bookkeeping
if ~isempty(obj.contrasts)
if size(obj.contrasts, 1) ~= obj.num_regressors && obj.num_regressors > 0
warning('glm_map:ContrastSize', 'contrasts has %d rows but design has %d regressors.', ...
size(obj.contrasts, 1), obj.num_regressors);
end
if ~isempty(obj.contrast_names) && numel(obj.contrast_names) ~= size(obj.contrasts, 2)
warning('glm_map:ContrastNames', 'Number of contrast_names (%d) does not match number of contrasts (%d).', ...
numel(obj.contrast_names), size(obj.contrasts, 2));
end
end

% Delegate to contained statistic_image maps where possible
for f = {'betas', 't', 'contrast_estimates', 'contrast_t'}
m = obj.(f{1});
if ~isempty(m) && isa(m, 'statistic_image') && ismethod(m, 'check_properties')
obj.(f{1}) = check_properties(m);
end
end

end % check_properties
Loading
Loading