Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENH] add QA for GLM #1135

Merged
merged 17 commits into from
Sep 17, 2023
5 changes: 0 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,6 @@ repos:
- id: black
args: [--config=pyproject.toml]

- repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.4.0
hooks:
- id: setup-cfg-fmt

- repo: https://github.com/asottile/pyupgrade
rev: v3.10.1
hooks:
Expand Down
2 changes: 1 addition & 1 deletion demos/face_repetition/test_face_rep.m
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
% resolutions_to_test = 2;
end

parfor iResolution = 1:numel(resolutions_to_test)
for iResolution = 1:numel(resolutions_to_test)

this_res = resolutions_to_test(iResolution);

Expand Down
2 changes: 1 addition & 1 deletion demos/openneuro/ds000001_aroma_run.m
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
clear;
clc;

COPY = false;
COPY = true;
SUBJECT_LEVEL_DO = true;

addpath(fullfile(pwd, '..', '..'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"Description": "BIDS stats model for linebisection / oververbgeneration / overwordrepetition / covertverbgeneration tasks",
"Input": {
"task": [
"overtverbgeneration"
"overtverbgeneration",
"overtwordrepetition"
],
"space": [
"MNI152NLin2009cAsym"
Expand Down
22 changes: 22 additions & 0 deletions lib/utils/range.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
function y = range(x,dim)
% Sample range.
%
% USAGE::
%
% Y = range(X) returns the range of the values in X.
%
% For a vector input, Y is the difference between the maximum and minimum values
% For a matrix input, Y is a vector containing the range for each column.
% For N-D arrays, range operates along the first non-singleton dimension.
%
% range treats NaNs as missing values, and ignores them.
%
% Y = range(X,DIM) operates along the dimension DIM.
%


if nargin < 2
y = max(x) - min(x);
else
y = max(x,[],dim) - min(x,[],dim);
end
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ requires-python = ">=3.8"
dependencies = [
"bsmschema",
"rich",
"rich-argparse"
]
classifiers = [
"Development Status :: 3 - Alpha",
Expand Down Expand Up @@ -126,5 +127,6 @@ module = [
"bsmschema.models",
"rich",
"rich.logging",
"rich_argparse",
]
ignore_missing_imports = true
35 changes: 26 additions & 9 deletions src/batches/stats/setBatchEstimateModel.m
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
function matlabbatch = setBatchEstimateModel(matlabbatch, opt, nodeName, contrastsList, groups)
function matlabbatch = setBatchEstimateModel(matlabbatch, opt, tmp, contrastsList, groups)
%
% Set up the estimate model batch for run/subject or group level GLM
%
% USAGE::
%
% matlabbatch = setBatchEstimateModel(matlabbatch, opt)
% % for subject level
% matlabbatch = setBatchEstimateModel(matlabbatch, opt, subLabel)
%
% % for group level
% matlabbatch = setBatchEstimateModel(matlabbatch, opt, nodeName, contrastsList, groups)
%
% :param matlabbatch:
% :type matlabbatch: structure
%
% :type opt: structure
% :type opt: structure
% :param opt: Options chosen for the analysis.
% See checkOptions.
%
Expand All @@ -29,29 +32,40 @@
switch nargin

% run / subject level
case 2
case 3

subLabel = tmp;

outputDir = getFFXdir(subLabel, opt);

printBatchName('estimate subject level fmri model', opt);

if iscell(matlabbatch)
opt.orderBatches.specify = 1;

spmMatFile = cfg_dep('fMRI model specification SPM file', ...
substruct( ...
'.', 'val', '{}', {1}, ...
substruct('.', 'val', '{}', {opt.orderBatches.specify}, ...
'.', 'val', '{}', {1}, ...
'.', 'val', '{}', {1}), ...
substruct('.', 'spmmat'));

elseif ischar(matlabbatch) && isdir(matlabbatch)
outputDir = matlabbatch;
spmMatFile = {fullfile(outputDir, 'SPM.mat')};
matlabbatch = {};
end

matlabbatch = returnEstimateModelBatch(matlabbatch, spmMatFile, opt);

opt.orderBatches.estimate = numel(matlabbatch);
matlabbatch = setBatchGoodnessOfFit(matlabbatch, opt);

matlabbatch = setBatchInformationCriteria(matlabbatch, opt, outputDir);

% group level
case 5

nodeName = tmp;

if ischar(contrastsList)
contrastsList = cellstr(contrastsList);
end
Expand All @@ -76,8 +90,11 @@
opt.QA.glm.do = false();

matlabbatch = returnEstimateModelBatch(matlabbatch, spmMatFile, opt);
opt.orderBatches.estimate = numel(matlabbatch);
matlabbatch = setBatchGoodnessOfFit(matlabbatch, opt);
matlabbatch = setBatchInformationCriteria(matlabbatch, opt, rfxDir);

matlabbatch{end + 1}.spm.stats.review.spmmat = spmMatFile;
matlabbatch{end + 1}.spm.stats.review.spmmat = spmMatFile; %#ok<*AGROW>
matlabbatch{end}.spm.stats.review.display.matrix = 1;
matlabbatch{end}.spm.stats.review.print = false;

Expand All @@ -89,7 +106,7 @@

otherwise

error('Should have 2 or 5 inputs...');
error('Should have 3 or 5 inputs...');

end

Expand Down
20 changes: 20 additions & 0 deletions src/batches/stats/setBatchGoodnessOfFit.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
function matlabbatch = setBatchGoodnessOfFit(matlabbatch, opt)

% (C) Copyright 2023 bidspm developers

if bids.internal.is_octave()
% https://github.com/cpp-lln-lab/bidspm/pull/1135#issuecomment-1722455363
notImplemented(mfilename(), ...
'Goodness of fit not implemented in Octave.', ...
opt);
return
end

MA_inspect_GoF.SPM_mat(1) = cfg_dep('Model estimation: SPM.mat File', ...
returnDependency(opt, 'estimate'), ...
substruct('.', 'spmmat'));
MA_inspect_GoF.plot_type = 1;

matlabbatch{end + 1}.spm.tools.MACS.MA_inspect_GoF = MA_inspect_GoF;

end
28 changes: 28 additions & 0 deletions src/batches/stats/setBatchInformationCriteria.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
function matlabbatch = setBatchInformationCriteria(matlabbatch, opt, outputDir)

% (C) Copyright 2023 bidspm developers
MA_model_space.dir = {outputDir};
MA_model_space.models{1}{1}(1) = cfg_dep('Model estimation: SPM.mat File', ...
returnDependency(opt, 'estimate'), ...
substruct('.', 'spmmat'));
MA_model_space.names = {'GLM'};

matlabbatch{end + 1}.spm.tools.MACS.MA_model_space = MA_model_space;

MA_classic_ICs_auto.MS_mat(1) = cfg_dep('MA: define model space: model space (MS.mat file)', ...
substruct('.', 'val', '{}', {numel(matlabbatch)}, ...
'.', 'val', '{}', {1}, ...
'.', 'val', '{}', {1}, ...
'.', 'val', '{}', {1}), ...
substruct('.', 'MS_mat'));
MA_classic_ICs_auto.ICs.AIC = 1;
MA_classic_ICs_auto.ICs.AICc = 0;
MA_classic_ICs_auto.ICs.BIC = 1;
MA_classic_ICs_auto.ICs.DIC = 0;
MA_classic_ICs_auto.ICs.HQC = 0;
MA_classic_ICs_auto.ICs.KIC = 0;
MA_classic_ICs_auto.ICs.KICc = 0;

matlabbatch{end + 1}.spm.tools.MACS.MA_classic_ICs_auto = MA_classic_ICs_auto;

end
21 changes: 6 additions & 15 deletions src/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

import argparse
import logging
from typing import IO

import rich
from rich.logging import RichHandler
from rich_argparse import RichHelpFormatter

from . import _version # type: ignore

Expand All @@ -29,24 +28,18 @@ def bidspm_log(name: str = "bidspm") -> logging.Logger:
return logging.getLogger(name)


class MuhParser(argparse.ArgumentParser):
def _print_message(self, message: str, file: IO[str] | None = None) -> None:
rich.print(message, file=file)


# TODO qplit in several parsers like in matlab


def common_parser() -> MuhParser:
parser = MuhParser(
def common_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="bidspm is a SPM base BIDS app",
epilog="""
\n- all parameters use ``snake_case``,

\n- most "invalid" calls simply initialize bidspm.

For a more readable version of this help section,
see the online https://bidspm.readthedocs.io/en/latest/usage_notes.html.
""",
formatter_class=RichHelpFormatter,
)

parser.add_argument(
Expand All @@ -67,8 +60,6 @@ def common_parser() -> MuhParser:
"output_dir",
help="""
Fullpath to the directory where the output files will be stored.
If you are running group level analysis this folder should be prepopulated
with the results of the participant level analysis.
""",
nargs=1,
)
Expand Down Expand Up @@ -311,7 +302,7 @@ def common_parser() -> MuhParser:

def validate_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="validate bids stats model",
description="validate bids stats model", formatter_class=RichHelpFormatter
)

parser.add_argument(
Expand Down
36 changes: 28 additions & 8 deletions src/utils/returnDependency.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,41 @@

% (C) Copyright 2021 bidspm developers

handledDependencies = {'segment', 'skullStripping', 'skullStrippingMask'...
'coregister', 'selectAnat', 'realign', ...
'MACS_model_space', 'MACS_BMS_group_auto', ...
'mean', 'smooth', 'mask', 'maskedMean', 'rename'};
handledDependencies = {'estoiamte', ...
'segment', ...
'skullStripping', ...
'skullStrippingMask'...
'coregister', ...
'selectAnat', ...
'realign', ...
'MACS_model_space', ...
'MACS_BMS_group_auto', ...
'mean', ...
'smooth', ...
'mask', ...
'maskedMean', ...
'rename'};

switch type

case {'segment', 'skullStripping', 'mean', 'smooth', 'mask', 'maskedMean', ...
'skullStrippingMask'}
case {'segment', ...
'skullStripping', ...
'mean', ...
'smooth', ...
'mask', ...
'maskedMean', ...
'skullStrippingMask', ...
'estimate'}
dep = substruct('.', 'val', '{}', {opt.orderBatches.(type)}, ...
'.', 'val', '{}', {1}, ...
'.', 'val', '{}', {1});

case {'coregister', 'selectAnat', 'realign', ...
'MACS_model_space', 'MACS_BMS_group_auto', 'rename'}
case {'coregister', ...
'selectAnat', ...
'realign', ...
'MACS_model_space', ...
'MACS_BMS_group_auto', ...
'rename'}
dep = substruct('.', 'val', '{}', {opt.orderBatches.(type)}, ...
'.', 'val', '{}', {1}, ...
'.', 'val', '{}', {1}, ...
Expand Down
5 changes: 1 addition & 4 deletions src/workflows/stats/bidsFFX.m
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 +248,7 @@ function checkRootNode(opt)
'before estimation', ...
subLabel)));
case 'estimate'
if isempty(matlabbatch)
matlabbatch = outputDir;
end
matlabbatch = setBatchEstimateModel(matlabbatch, opt);
matlabbatch = setBatchEstimateModel(matlabbatch, opt, subLabel);
matlabbatch = setBatchPrintFigure(matlabbatch, opt, ...
fullfile(outputDir, ...
designMatrixFigureName(opt, ...
Expand Down
15 changes: 12 additions & 3 deletions tests/data/models/model-nback_smdl.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
],
"Model": {
"X": [
"nback"
"trial_type.scene*",
"trial_type.face*"
],
"Type": "glm",
"HRF": {
"Variables": [
""
"trial_type.scene*",
"trial_type.face*"
],
"Model": "spm"
},
Expand All @@ -36,6 +38,12 @@
"mask"
]
}
},
"Software": {
"SPM": {
"InclusiveMaskingThreshold": 0.8,
"SerialCorrelation": "FAST"
}
}
},
"Contrasts": [
Expand All @@ -53,7 +61,8 @@
"DummyContrasts": {
"Test": "t",
"Contrasts": [
""
"trial_type.scene*",
"trial_type.face*"
]
}
},
Expand Down
6 changes: 3 additions & 3 deletions tests/tests_batches/stats/test_setBatchSubjectLevelGLMSpec.m
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ function test_setBatchSubjectLevelGLMSpec_fmriprep()
opt = checkOptions(opt);

matlabbatch = {};
% bids matlab issue?
% events.TSV are in the root of the synthetic dataset
% matlabbatch = setBatchSubjectLevelGLMSpec(matlabbatch, BIDS, opt, subLabel);

BIDS = getLayout(opt);
matlabbatch = setBatchSubjectLevelGLMSpec(matlabbatch, BIDS, opt, subLabel);

end

Expand Down
Loading
Loading