From b7ebdc9e40e25ba90f0001160f31a9d096d0256e Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 11 Aug 2023 16:24:50 -0400 Subject: [PATCH] [ENH] add model selection to CLI (#1121) * update submod * [DATALAD] Recorded changes * [DATALAD] Recorded changes * minor work on cli * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix some tests * more fixes * add folders * start implementing bms * fix system test * loop over models * rm models * generate models * update bids matlab * Apply suggestions from code review * fix bug * fix test * refacrtor * refactor * add basic error handling * fix test * fix and doc * fix * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * move space to each action cli * add use_dummy_regressor arg * [DATALAD] Recorded changes * move space to each action cli * [DATALAD] Recorded changes * update fields name, fix failing tests, add arg displaying function * refactor CI * FCI * smooth bms data * use the proper task * use dummy regressor argument * [DATALAD] Recorded changes * use proper argument * fix passing or use fummy argument --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/system_tests_bms.m | 12 + .github/workflows/tests.yml | 23 +- bidspm.m | 61 ++-- demos/MoAE/moae_01_bids_app.m | 2 +- demos/MoAE/test_moae.m | 6 +- demos/bayes/Makefile | 11 + demos/bayes/ds000114_run.m | 150 +++++++++ demos/bayes/inputs/.gitkeep | 0 demos/bayes/miss_hit.cfg | 2 + demos/bayes/models/.gitignore | 1 + demos/bayes/models/default_model.json | 60 ++++ demos/bayes/outputs/.gitkeep | 0 demos/bayes/spm_my_defaults.m | 7 + demos/face_repetition/face_rep_01_bids_app.m | 2 +- demos/face_repetition/test_face_rep.m | 2 +- demos/openneuro/Makefile | 1 - demos/openneuro/ds000001_run.m | 2 +- demos/openneuro/ds001168_run.m | 2 +- ...114_desc-testRetestLineBisection_smdl.json | 63 ++-- ...l-ds000114_desc-testRetestVerbal_smdl.json | 55 ++-- ...model-ds000224_desc-glasslexical_smdl.json | 103 ++++-- docs/source/default_options.m | 2 +- docs/source/installation.rst | 17 +- docs/source/usage_notes.rst | 4 +- lib/bids-matlab | 2 +- src/IO/getData.m | 2 +- src/QA/compileScrubbingStats.m | 3 + src/batches/preproc/setBatchSmoothingFunc.m | 4 + .../stats/setBatchSubjectLevelGLMSpec.m | 7 +- src/bids/getBoldFilename.m | 8 +- src/cli/baseInputParser.m | 2 - src/cli/cliBayesModel.m | 30 ++ src/cli/cliCopy.m | 7 +- src/cli/cliCreateRoi.m | 10 +- src/cli/cliDefaultModel.m | 9 +- src/cli/cliPreprocess.m | 14 +- src/cli/cliSmooth.m | 7 +- src/cli/cliStats.m | 118 ++++--- src/cli/getOptionsFromCliArgument.m | 67 +++- src/cli/inputParserForBayesModel.m | 21 ++ src/cli/inputParserForCopy.m | 4 + src/cli/inputParserForCreateModel.m | 5 +- src/cli/inputParserForCreateRoi.m | 1 + src/cli/inputParserForPreprocess.m | 4 + src/cli/inputParserForStats.m | 7 +- src/defaults/checkOptions.m | 29 +- src/defaults/getOptionsFromModel.m | 21 +- src/messages/bidspmHelp.m | 301 +++++++++++------- src/reports/boilerplate.m | 6 +- .../createAndReturnCounfoundMatFile.m | 5 + .../orderAndPadCounfoundMatFile.m | 7 + src/utils/displayArguments.m | 9 + src/utils/setUpWorkflow.m | 3 +- src/workflows/bidsCopyInputFolder.m | 3 + src/workflows/bidsReport.m | 2 +- src/workflows/preproc/bidsSmoothing.m | 4 + src/workflows/stats/bidsModelSelection.m | 5 +- tests/data/models/model-default_smdl.json | 2 +- tests/data/models/model-nback_smdl.json | 2 +- tests/tests_bids_model/test_bidsModel.m | 6 +- .../test_getOptionsFromModel.m | 9 + tests/tests_cli/test_bidspm_boilderplate.m | 2 +- .../test_bidspm_default_stats_model.m | 9 +- tests/tests_defaults/test_checkOptions.m | 7 +- tests/tests_reports/test_boilerplate.m | 6 +- .../test_getEventSpecificationRoiGlm.m | 2 +- .../subject_level/test_specifyContrasts.m | 12 +- .../unit_tests/test_getInclusiveMask.m | 2 +- .../tests_stats/utils/test_getRegressorIdx.m | 2 +- .../tests_unit/test_copyGraphWindownOutput.m | 2 +- tests/tests_unit/test_displayArguments.m | 13 + tests/tests_unit/test_removeDummies.m | 2 +- tests/tests_unit/test_utils.m | 3 +- tests/tests_unit/test_warnings.m | 8 +- tests/utils/defaultOptions.m | 7 +- 75 files changed, 1038 insertions(+), 373 deletions(-) create mode 100644 .github/workflows/system_tests_bms.m create mode 100644 demos/bayes/Makefile create mode 100644 demos/bayes/ds000114_run.m create mode 100644 demos/bayes/inputs/.gitkeep create mode 100644 demos/bayes/miss_hit.cfg create mode 100644 demos/bayes/models/.gitignore create mode 100644 demos/bayes/models/default_model.json create mode 100644 demos/bayes/outputs/.gitkeep create mode 100644 demos/bayes/spm_my_defaults.m create mode 100644 src/cli/cliBayesModel.m create mode 100644 src/cli/inputParserForBayesModel.m create mode 100644 src/utils/displayArguments.m create mode 100644 tests/tests_unit/test_displayArguments.m diff --git a/.github/workflows/system_tests_bms.m b/.github/workflows/system_tests_bms.m new file mode 100644 index 000000000..6093cb191 --- /dev/null +++ b/.github/workflows/system_tests_bms.m @@ -0,0 +1,12 @@ +% +% (C) Copyright 2023 bidspm developers + +root_dir = getenv('GITHUB_WORKSPACE'); + +fprintf('\nroot dir is %s\n', root_dir); + +addpath(fullfile(root_dir, 'spm12')); + +cd(fullfile(root_dir, 'demos', 'bayes')); + +run ds000114_run; diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index efedf8189..09041d74d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -43,6 +43,10 @@ jobs: os: [ubuntu-latest, macos-latest] matlab: [R2022b] include: + - test_type: system + script: bms + os: ubuntu-latest + matlab: R2022b - test_type: unit os: ubuntu-latest matlab: R2022b @@ -59,12 +63,6 @@ jobs: if: matrix.test_type == 'system' run: echo ${{ matrix.test_type }} test ${{ matrix.script }} - - name: Install dependencies - if: matrix.os == 'ubuntu-latest' - run: | - sudo apt-get -y -qq update - sudo apt-get -y install unzip wget - - name: Install Node uses: actions/setup-node@v3 with: @@ -75,6 +73,14 @@ jobs: with: python-version: '3.11' + - name: Install dependencies + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get -y -qq update + sudo apt-get -y install unzip wget git-annex + python -m pip install --upgrade pip setuptools + pip install datalad + - name: Clone bidspm uses: actions/checkout@v3 with: @@ -104,6 +110,10 @@ jobs: cd tests make data + - name: Get data for BMS + if: matrix.script == 'bms' + run: make -C demos/bayes data_ds000114 + - name: Install Moxunit and MOcov run: | git clone https://github.com/MOxUnit/MOxUnit.git --depth 1 @@ -130,7 +140,6 @@ jobs: fail_ci_if_error: false # token: ${{ secrets.CODECOV_TOKEN }} # not required but might help API rate limits - - name: Run system tests MATLAB ${{ matrix.script }} if: matrix.test_type == 'system' uses: matlab-actions/run-command@v1.2.1 diff --git a/bidspm.m b/bidspm.m index 2e7d5f2e1..f0ad11fda 100644 --- a/bidspm.m +++ b/bidspm.m @@ -6,7 +6,12 @@ % (C) Copyright 2022 bidspm developers args = defaultInputParser(); - parse(args, varargin{:}); + try + parse(args, varargin{:}); + catch ME + displayArguments(varargin{:}); + rethrow(ME); + end action = args.Results.action; @@ -45,7 +50,7 @@ case 'help' system('bidspm --help'); - help(fullfile(fileparts(mfilename('fullpath')), 'src', 'messages', 'bidspmHelp.m')); + help(fullfile(rootDir(), 'src', 'messages', 'bidspmHelp.m')); case 'version' versionBidspm(); @@ -77,9 +82,12 @@ case 'default_model' cliDefaultModel(varargin{2:end}); - case {'stats', 'contrasts', 'results'} + case {'stats', 'contrasts', 'results', 'specify_only'} cliStats(varargin{2:end}); + case {'bms'} + cliBayesModel(varargin{2:end}); + case 'meaning_of_life' fprintf('\n42\n\n'); @@ -94,12 +102,25 @@ end +function displayArguments(varargin) + disp('arguments passed were :'); + for i = 1:numel(varargin) + fprintf('- '); + disp(varargin{i}); + end + fprintf(1, '\n'); +end + +function value = rootDir() + value = fullfile(fileparts(mfilename('fullpath'))); +end + %% low level actions function versionBidspm() try versionNumber = getVersion(); catch - versionNumber = fileread(fullfile(fileparts(mfilename('fullpath')), 'version.txt')); + versionNumber = fileread(rootDir(), 'version.txt'); versionNumber = versionNumber(1:end - 1); end fprintf(1, '%s\n', versionNumber); @@ -130,8 +151,6 @@ function initBidspm(dev) % octave packages installlist = {'io', 'statistics', 'image'}; - thisDirectory = fileparts(mfilename('fullpath')); - global BIDSPM_INITIALIZED global BIDSPM_PATHS @@ -143,19 +162,19 @@ function initBidspm(dev) end % add bidspm source code - BIDSPM_PATHS = fullfile(thisDirectory); + BIDSPM_PATHS = fullfile(rootDir()); BIDSPM_PATHS = cat(2, BIDSPM_PATHS, ... pathSep, ... - genpath(fullfile(thisDirectory, 'src'))); + genpath(fullfile(rootDir(), 'src'))); if dev BIDSPM_PATHS = cat(2, BIDSPM_PATHS, pathSep, ... - fullfile(thisDirectory, 'tests', 'utils')); + fullfile(rootDir(), 'tests', 'utils')); end % for some reasons this folder was otherwise not added to the path in Octave BIDSPM_PATHS = cat(2, BIDSPM_PATHS, ... pathSep, ... - genpath(fullfile(thisDirectory, 'src', 'workflows', 'stats'))); + genpath(fullfile(rootDir(), 'src', 'workflows', 'stats'))); % add library that do not have an set up script libList = {'spmup'}; @@ -163,7 +182,7 @@ function initBidspm(dev) for i = 1:numel(libList) BIDSPM_PATHS = cat(2, BIDSPM_PATHS, ... pathSep, ... - genpath(fullfile(thisDirectory, 'lib', libList{i}))); + genpath(fullfile(rootDir(), 'lib', libList{i}))); end libList = {'mancoreg', ... @@ -173,23 +192,23 @@ function initBidspm(dev) 'utils'}; for i = 1:numel(libList) BIDSPM_PATHS = cat(2, BIDSPM_PATHS, pathSep, ... - fullfile(thisDirectory, 'lib', libList{i})); + fullfile(rootDir(), 'lib', libList{i})); end BIDSPM_PATHS = cat(2, BIDSPM_PATHS, pathSep, ... - fullfile(thisDirectory, 'lib', 'brain_colours', 'code')); + fullfile(rootDir(), 'lib', 'brain_colours', 'code')); BIDSPM_PATHS = cat(2, BIDSPM_PATHS, pathSep, ... - fullfile(thisDirectory, 'lib', 'riksneurotools', 'GLM')); + fullfile(rootDir(), 'lib', 'riksneurotools', 'GLM')); addpath(BIDSPM_PATHS, '-begin'); silenceOctaveWarning(); % add library that have a set up script - run(fullfile(thisDirectory, 'lib', 'CPP_ROI', 'initCppRoi')); - run(fullfile(thisDirectory, 'lib', 'spm_2_bids', 'init_spm_2_bids')); - run(fullfile(thisDirectory, 'lib', 'octache', 'setup')); + run(fullfile(rootDir(), 'lib', 'CPP_ROI', 'initCppRoi')); + run(fullfile(rootDir(), 'lib', 'spm_2_bids', 'init_spm_2_bids')); + run(fullfile(rootDir(), 'lib', 'octache', 'setup')); checkDependencies(opt); printCredits(opt); @@ -248,8 +267,6 @@ function uninitBidspm() % (C) Copyright 2021 bidspm developers - thisDirectory = fileparts(mfilename('fullpath')); - global BIDSPM_INITIALIZED global BIDSPM_PATHS @@ -258,7 +275,7 @@ function uninitBidspm() return else - run(fullfile(thisDirectory, 'lib', 'CPP_ROI', 'uninitCppRoi')); + run(fullfile(rootDir(), 'lib', 'CPP_ROI', 'uninitCppRoi')); rmpath(BIDSPM_PATHS); spm('Clean'); spm('Quit'); @@ -350,7 +367,9 @@ function update() 'default_model'; ... 'stats'; ... 'contrasts'; ... - 'results'}; + 'results'; ... + 'specify_only', ... + 'bms'}; end diff --git a/demos/MoAE/moae_01_bids_app.m b/demos/MoAE/moae_01_bids_app.m index 99b5e91bd..d27bc2c95 100644 --- a/demos/MoAE/moae_01_bids_app.m +++ b/demos/MoAE/moae_01_bids_app.m @@ -124,7 +124,7 @@ bidspm(bids_dir, output_dir, 'subject', ... 'participant_label', {subject_label}, ... 'action', 'preprocess', ... - 'task', {'auditory'}, ... + 'task', 'auditory', ... 'ignore', {'unwarp', 'slicetiming'}, ... 'space', {'IXI549Space'}, ... 'fwhm', 6, ... diff --git a/demos/MoAE/test_moae.m b/demos/MoAE/test_moae.m index a8b0bc535..e8427c5d7 100644 --- a/demos/MoAE/test_moae.m +++ b/demos/MoAE/test_moae.m @@ -24,7 +24,7 @@ bidspm(); if download_data - download_moae_ds(download_data, clean); + download_moae_ds(download_data, clean); %#ok<*UNRCH> end warning('off', 'SPM:noDisplay'); @@ -52,12 +52,12 @@ %% preproc bids_dir = fullfile(WD, 'inputs', 'raw'); output_dir = fullfile(tempname, 'outputs', 'derivatives'); - mkdir(output_dir); + spm_mkdir(output_dir); bidspm(bids_dir, output_dir, 'subject', ... 'participant_label', {'01'}, ... 'action', 'preprocess', ... - 'task', {'auditory'}, ... + 'task', 'auditory', ... 'ignore', ignore{iOption}, ... 'space', space(iOption), ... 'options', optionsFile); diff --git a/demos/bayes/Makefile b/demos/bayes/Makefile new file mode 100644 index 000000000..8738ec9ea --- /dev/null +++ b/demos/bayes/Makefile @@ -0,0 +1,11 @@ +.PHONY: clean data data_ds000001 + +data_ds000114: + mkdir -p inputs + cd inputs && datalad install ///openneuro/ds000114 + cd inputs && datalad install ///openneuro-derivatives/ds000114-fmriprep + cd inputs/ds000114-fmriprep && datalad get sub-*/anat/*MNI152NLin2009cAsym*desc-preproc*.nii.gz -J 12 + cd inputs/ds000114-fmriprep && datalad get sub-*/ses-*/func/*tsv -J 12 + cd inputs/ds000114-fmriprep && datalad get sub-*/ses-*/func/*json -J 12 + cd inputs/ds000114-fmriprep && datalad get sub-*/ses-*/func/*overtverbgeneration*MNI152NLin2009cAsym*_mask.nii.gz -J 12 + cd inputs/ds000114-fmriprep && datalad get sub-*/ses-*/func/*overtverbgeneration*MNI152NLin2009cAsym*desc-preproc*bold.nii.gz -J 12 diff --git a/demos/bayes/ds000114_run.m b/demos/bayes/ds000114_run.m new file mode 100644 index 000000000..5ba628d08 --- /dev/null +++ b/demos/bayes/ds000114_run.m @@ -0,0 +1,150 @@ +% (C) Copyright 2023 bidspm developers + +clear; +clc; + +addpath(fullfile(pwd, '..', '..')); +bidspm(); + +% set to false to not re run the smoothing +SMOOTH = true; + +% set to false to not re run the model specification +FIRST_LEVEL = true; + +VERBOSITY = 1; + +FWHM = 8; + +% to run on fewer subjects +TESTING = true; + +% The directory where the data are located +root_dir = fileparts(mfilename('fullpath')); +bids_dir = fullfile(root_dir, 'inputs', 'ds000114'); +fmriprep_dir = fullfile(root_dir, 'inputs', 'ds000114-fmriprep'); +output_dir = fullfile(root_dir, 'outputs', 'ds000114', 'derivatives'); + +models_dir = fullfile(root_dir, 'models'); + +participant_label = {'[0-9]*'}; +if TESTING + participant_label = {'^0[12]$'}; +end + +%% Smooth +if SMOOTH + % only need to smooth the functional data + opt.query.modality = {'func'}; + bidspm(fmriprep_dir, output_dir, 'subject', ... + 'action', 'smooth', ... + 'participant_label', participant_label, ... + 'task', {'overtverbgeneration'}, ... + 'space', {'MNI152NLin2009cAsym'}, ... + 'fwhm', FWHM, ... + 'verbosity', VERBOSITY, ... + 'options', opt); %#ok<*UNRCH> +end + +%% create models from a default one + +default_model_file = fullfile(models_dir, 'default_model.json'); + +mutliverse.strategy = {'motion', 'wm_csf', 'scrub', 'non_steady_state'}; +mutliverse.motion = {'none', 'basic', 'full'}; +mutliverse.scrub = [false, true]; +mutliverse.wm_csf = {'none', 'basic', 'full'}; +mutliverse.non_steady_state = [false, true]; + +create_model_families(models_dir, default_model_file, mutliverse); + +%% Statistics +preproc_dir = fullfile(output_dir, 'bidspm-preproc'); + +%% Subject level analysis +if FIRST_LEVEL + + bidspm(bids_dir, output_dir, 'subject', ... + 'participant_label', participant_label, ... + 'action', 'specify_only', ... + 'preproc_dir', preproc_dir, ... + 'model_file', models_dir, ... + 'fwhm', FWHM, ... + 'skip_validation', true, ... + 'use_dummy_regressor', true, ... + 'verbosity', VERBOSITY); + +end + +bidspm(bids_dir, output_dir, 'subject', ... + 'action', 'bms', ... + 'participant_label', participant_label, ... + 'models_dir', models_dir, ... + 'fwhm', FWHM, ... + 'skip_validation', true, ... + 'verbosity', VERBOSITY); + +%% +function create_model_families(models_dir, default_model_file, mutliverse) + % create models from a default one + % + + % TODO incorporate into bidspm + + % TODO add support for 12 motion regressors + + for i = 1:numel(mutliverse.motion) + for j = 1:numel(mutliverse.scrub) + for k = 1:numel(mutliverse.wm_csf) + for l = 1:numel(mutliverse.non_steady_state) + + model = bids.util.jsondecode(default_model_file); + + name = sprintf('rp-%s_scrub-%i_tissue-%s_nsso-%i', ... + mutliverse.motion{i}, ... + mutliverse.scrub(j), ... + mutliverse.wm_csf{k}, ... + mutliverse.non_steady_state(l)); + model.Name = name; + model.Nodes.Name = name; + + design_matrix = model.Nodes.Model.X; + + switch mutliverse.motion{i} + case 'none' + case 'basic' + design_matrix{end + 1} = 'rot_?'; + design_matrix{end + 1} = 'trans_?'; + case 12 + case 'full' + design_matrix{end + 1} = 'rot_*'; + design_matrix{end + 1} = 'trans_*'; + end + + if mutliverse.scrub(j) == 1 + design_matrix{end + 1} = 'motion_outlier*'; %#ok<*AGROW> + end + + switch mutliverse.wm_csf{k} + case 'none' + case 'basic' + design_matrix{end + 1} = 'csf'; + design_matrix{end + 1} = 'white'; + case 'full' + design_matrix{end + 1} = 'csf_*'; + design_matrix{end + 1} = 'white_*'; + end + + if mutliverse.non_steady_state(l) + design_matrix{end + 1} = 'non_steady_state_outlier*'; + end + + model.Nodes.Model.X = design_matrix; + + output_file = fullfile(models_dir, ['model_' name '_smdl.json']); + bids.util.jsonencode(output_file, model); + end + end + end + end +end diff --git a/demos/bayes/inputs/.gitkeep b/demos/bayes/inputs/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/demos/bayes/miss_hit.cfg b/demos/bayes/miss_hit.cfg new file mode 100644 index 000000000..916ed6ec4 --- /dev/null +++ b/demos/bayes/miss_hit.cfg @@ -0,0 +1,2 @@ +exclude_dir: "inputs" +exclude_dir: "outputs" diff --git a/demos/bayes/models/.gitignore b/demos/bayes/models/.gitignore new file mode 100644 index 000000000..53f42db4d --- /dev/null +++ b/demos/bayes/models/.gitignore @@ -0,0 +1 @@ +*smdl.json diff --git a/demos/bayes/models/default_model.json b/demos/bayes/models/default_model.json new file mode 100644 index 000000000..99cb28f6b --- /dev/null +++ b/demos/bayes/models/default_model.json @@ -0,0 +1,60 @@ +{ + "Name": "default model", + "BIDSModelVersion": "1.0.0", + "Description": "FIXME", + "Input": { + "task": [ + "overtverbgeneration" + ], + "space": [ + "MNI152NLin2009cAsym" + ] + }, + "Nodes": [ + { + "Level": "Run", + "Name": "FIXME", + "GroupBy": [ + "run", + "session", + "subject" + ], + "Model": { + "X": [ + "trial_type.Task", + 1 + ], + "Type": "glm", + "HRF": { + "Variables": [ + "trial_type.Task" + ], + "Model": "spm" + }, + "Options": { + "HighPassFilterCutoffHz": 0.008, + "Mask": { + "desc": [ + "brain" + ], + "suffix": [ + "mask" + ], + "task": [ + "overtverbgeneration" + ], + "space": [ + "MNI152NLin2009cAsym" + ] + } + }, + "Software": { + "SPM": { + "SerialCorrelation": "FAST", + "InclusiveMaskingThreshold": 0.8 + } + } + } + } + ] +} diff --git a/demos/bayes/outputs/.gitkeep b/demos/bayes/outputs/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/demos/bayes/spm_my_defaults.m b/demos/bayes/spm_my_defaults.m new file mode 100644 index 000000000..cb038332d --- /dev/null +++ b/demos/bayes/spm_my_defaults.m @@ -0,0 +1,7 @@ +function spm_my_defaults + % (C) Copyright 2023 bidspm developers + global defaults + + defaults.cmdline = true; + +end diff --git a/demos/face_repetition/face_rep_01_bids_app.m b/demos/face_repetition/face_rep_01_bids_app.m index 2e41811c9..9568127c1 100644 --- a/demos/face_repetition/face_rep_01_bids_app.m +++ b/demos/face_repetition/face_rep_01_bids_app.m @@ -52,6 +52,6 @@ bidspm(bids_dir, output_dir, 'subject', ... 'participant_label', {subject_label}, ... 'action', 'preprocess', ... - 'task', {'facerepetition'}, ... + 'task', 'facerepetition', ... 'space', {'individual', 'IXI549Space'}, ... 'skip_validation', skip_validation); diff --git a/demos/face_repetition/test_face_rep.m b/demos/face_repetition/test_face_rep.m index 4987b47f6..04e514fc3 100644 --- a/demos/face_repetition/test_face_rep.m +++ b/demos/face_repetition/test_face_rep.m @@ -66,7 +66,7 @@ bidspm(bids_dir, output_dir, 'subject', ... 'participant_label', {'01'}, ... 'action', 'preprocess', ... - 'task', {'facerepetition'}, ... + 'task', 'facerepetition', ... 'space', {'IXI549Space'}, ... 'ignore', ignore, ... 'options', opt, ... diff --git a/demos/openneuro/Makefile b/demos/openneuro/Makefile index e55a12f4c..7d8070c87 100644 --- a/demos/openneuro/Makefile +++ b/demos/openneuro/Makefile @@ -60,7 +60,6 @@ data_ds001734: cd inputs/ds001734-fmriprep && datalad get sub-00[1]/func/*json -J 12 cd inputs/ds001734-fmriprep && datalad get sub-00[1]/func/*MNI152NLin2009cAsym*desc-preproc*bold.nii.gz -J 12 - data_ds002799: mkdir -p inputs cd inputs && datalad install ///openneuro/ds002799 diff --git a/demos/openneuro/ds000001_run.m b/demos/openneuro/ds000001_run.m index 782ae996f..b1791edeb 100644 --- a/demos/openneuro/ds000001_run.m +++ b/demos/openneuro/ds000001_run.m @@ -18,7 +18,7 @@ bidspm(bids_dir, output_dir, 'subject', ... 'participant_label', participant_label(i_participant), ... 'action', 'preprocess', ... - 'task', {'balloonanalogrisktask'}, ... + 'task', 'balloonanalogrisktask', ... 'space', {'IXI549Space'}); end diff --git a/demos/openneuro/ds001168_run.m b/demos/openneuro/ds001168_run.m index bb051ef75..b1bb7d14d 100644 --- a/demos/openneuro/ds001168_run.m +++ b/demos/openneuro/ds001168_run.m @@ -32,7 +32,7 @@ bidspm(bids_dir, output_dir, 'subject', ... 'participant_label', {'01'}, ... 'action', 'preprocess', ... - 'task', {'rest'}, ... + 'task', 'rest', ... 'space', {'IXI549Space'}, ... 'ignore', {'slicetiming'}, ... 'options', opt); diff --git a/demos/openneuro/models/model-ds000114_desc-testRetestLineBisection_smdl.json b/demos/openneuro/models/model-ds000114_desc-testRetestLineBisection_smdl.json index 90c245ab5..fa5847021 100644 --- a/demos/openneuro/models/model-ds000114_desc-testRetestLineBisection_smdl.json +++ b/demos/openneuro/models/model-ds000114_desc-testRetestLineBisection_smdl.json @@ -6,7 +6,9 @@ "task": [ "linebisection" ], - "space": ["MNI152NLin2009cAsym"] + "space": [ + "MNI152NLin2009cAsym" + ] }, "Nodes": [ { @@ -42,8 +44,12 @@ "Options": { "HighPassFilterCutoffHz": 0.008, "Mask": { - "desc": ["brain"], - "suffix": ["mask"] + "desc": [ + "brain" + ], + "suffix": [ + "mask" + ] } }, "Software": { @@ -65,18 +71,18 @@ }, "Contrasts": [ { - "Name": "TaskResponded_gt_CtrlResponded", - "ConditionList": [ - "trial_type.Correct_Task", - "trial_type.Incorrect_Task", - "trial_type.Response_Control" - ], - "Weights": [ - 0.5, - 0.5, - -1 - ], - "Test": "t" + "Name": "TaskResponded_gt_CtrlResponded", + "ConditionList": [ + "trial_type.Correct_Task", + "trial_type.Incorrect_Task", + "trial_type.Response_Control" + ], + "Weights": [ + 0.5, + 0.5, + -1 + ], + "Test": "t" } ] }, @@ -89,7 +95,9 @@ "contrast" ], "Model": { - "X": [1], + "X": [ + 1 + ], "Type": "glm" }, "DummyContrasts": { @@ -104,7 +112,9 @@ "contrast" ], "Model": { - "X": [1], + "X": [ + 1 + ], "Type": "glm" }, "DummyContrasts": { @@ -119,15 +129,24 @@ "contrast" ], "Model": { - "X": ["session", 1], + "X": [ + "session", + 1 + ], "Type": "glm" }, "Contrasts": [ { - "Name": "test_gt_retest", - "ConditionList": ["test", "retest"], - "Weights": [1, -1], - "Test": "t" + "Name": "test_gt_retest", + "ConditionList": [ + "test", + "retest" + ], + "Weights": [ + 1, + -1 + ], + "Test": "t" } ] } diff --git a/demos/openneuro/models/model-ds000114_desc-testRetestVerbal_smdl.json b/demos/openneuro/models/model-ds000114_desc-testRetestVerbal_smdl.json index 0767d275a..02a8f5eba 100644 --- a/demos/openneuro/models/model-ds000114_desc-testRetestVerbal_smdl.json +++ b/demos/openneuro/models/model-ds000114_desc-testRetestVerbal_smdl.json @@ -6,7 +6,9 @@ "task": [ "overtverbgeneration" ], - "space": ["MNI152NLin2009cAsym"] + "space": [ + "MNI152NLin2009cAsym" + ] }, "Nodes": [ { @@ -34,8 +36,12 @@ "Options": { "HighPassFilterCutoffHz": 0.008, "Mask": { - "desc": ["brain"], - "suffix": ["mask"] + "desc": [ + "brain" + ], + "suffix": [ + "mask" + ] } }, "Software": { @@ -47,14 +53,14 @@ }, "Contrasts": [ { - "Name": "Task", - "ConditionList": [ - "trial_type.Task" - ], - "Weights": [ - 1 - ], - "Test": "t" + "Name": "Task", + "ConditionList": [ + "trial_type.Task" + ], + "Weights": [ + 1 + ], + "Test": "t" } ] }, @@ -66,7 +72,9 @@ "contrast" ], "Model": { - "X": [1], + "X": [ + 1 + ], "Type": "glm" }, "DummyContrasts": { @@ -82,7 +90,9 @@ "contrast" ], "Model": { - "X": [1], + "X": [ + 1 + ], "Type": "glm" }, "DummyContrasts": { @@ -97,15 +107,24 @@ "contrast" ], "Model": { - "X": ["session", 1], + "X": [ + "session", + 1 + ], "Type": "glm" }, "Contrasts": [ { - "Name": "test_gt_retest", - "ConditionList": ["test", "retest"], - "Weights": [1, -1], - "Test": "t" + "Name": "test_gt_retest", + "ConditionList": [ + "test", + "retest" + ], + "Weights": [ + 1, + -1 + ], + "Test": "t" } ] } diff --git a/demos/openneuro/models/model-ds000224_desc-glasslexical_smdl.json b/demos/openneuro/models/model-ds000224_desc-glasslexical_smdl.json index b2a5c91af..96990c81f 100644 --- a/demos/openneuro/models/model-ds000224_desc-glasslexical_smdl.json +++ b/demos/openneuro/models/model-ds000224_desc-glasslexical_smdl.json @@ -3,8 +3,12 @@ "BIDSModelVersion": "1.0.0", "Description": "default BIDS stats model for glasslexical task", "Input": { - "task": ["glasslexical"], - "space": ["MNI152NLin2009cAsym"] + "task": [ + "glasslexical" + ], + "space": [ + "MNI152NLin2009cAsym" + ] }, "Nodes": [ { @@ -52,8 +56,12 @@ "Options": { "HighPassFilterCutoffHz": 0.008, "Mask": { - "desc": ["brain"], - "suffix": ["mask"] + "desc": [ + "brain" + ], + "suffix": [ + "mask" + ] } }, "Software": { @@ -72,29 +80,29 @@ }, "Contrasts": [ { - "Name": "Verb_gt_Noun", - "ConditionList": [ + "Name": "Verb_gt_Noun", + "ConditionList": [ "trial_type.Noun", "trial_type.Verb" - ], - "Weights": [ - -1, - 1 - ], - "Test": "t" + ], + "Weights": [ + -1, + 1 + ], + "Test": "t" }, { "Name": "Noun_gt_Verb", "ConditionList": [ - "trial_type.Noun", - "trial_type.Verb" + "trial_type.Noun", + "trial_type.Verb" ], "Weights": [ - 1, - -1 + 1, + -1 ], "Test": "t" - } + } ] }, { @@ -106,7 +114,9 @@ "contrast" ], "Model": { - "X": [1], + "X": [ + 1 + ], "Type": "glm" }, "DummyContrasts": { @@ -121,7 +131,9 @@ "contrast" ], "Model": { - "X": [1], + "X": [ + 1 + ], "Type": "glm" }, "DummyContrasts": { @@ -136,21 +148,42 @@ "contrast" ], "Model": { - "X": ["session", 1], + "X": [ + "session", + 1 + ], "Type": "glm" }, - "Contrasts": [{ - "Name": "OddSessions_gt_EvenSessions", - "ConditionList": [ - "func01", "func03", "func05", "func07", "func09", - "func02", "func04", "func06", "func08", "func10" - ], - "Weights": [ - 1, 1, 1, 1, 1, - -1, -1, -1, -1, -1 - ], - "Test": "t" - }] + "Contrasts": [ + { + "Name": "OddSessions_gt_EvenSessions", + "ConditionList": [ + "func01", + "func03", + "func05", + "func07", + "func09", + "func02", + "func04", + "func06", + "func08", + "func10" + ], + "Weights": [ + 1, + 1, + 1, + 1, + 1, + -1, + -1, + -1, + -1, + -1 + ], + "Test": "t" + } + ] } ], "Edges": [ @@ -161,7 +194,11 @@ { "Source": "session", "Destination": "compare_sessions", - "Filter": {"contrast": ["Noun_gt_Verb"]} + "Filter": { + "contrast": [ + "Noun_gt_Verb" + ] + } }, { "Source": "run", diff --git a/docs/source/default_options.m b/docs/source/default_options.m index 264e53193..0dd4f6614 100644 --- a/docs/source/default_options.m +++ b/docs/source/default_options.m @@ -28,7 +28,7 @@ opt.dir.roi = 'derivatives/bidspm-roi' ; opt.dir.stats = '' ; opt.dryRun = 0, ; -opt.dummy_scans = 0.000, ; +opt.dummyScans = 0.000, ; opt.funcVolToSelect = [] ; opt.funcVoxelDims = [] ; opt.fwhm.contrast = 0.000, ; diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 1e0cb11da..3d05c4fad 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -6,13 +6,13 @@ Dependencies This SPM toolbox runs with Matlab and Octave. -============ ================ ====================== +============ ================ ========================================= Dependencies Minimum required Used for testing in CI -============ ================ ====================== -MATLAB 2014 2020a on Ubuntu 22.04 -Octave 6.4.0 6.4.0 on Ubuntu 20.04 +============ ================ ========================================= +MATLAB 2014 2022a on Ubuntu 22.04, Windowns and MacOS +Octave 6.4.0 6.4.0 on Ubuntu 22.04 SPM12 7219 7771 -============ ================ ====================== +============ ================ ========================================= Some functionalities require some extra SPM toolbox to work: for example the ALI toolbox for brain lesion segmentation. @@ -47,13 +47,6 @@ with the following git command: If you need the latest development, then you must clone from the ``dev`` branch: -.. code-block:: bash - - git clone --branch dev --recurse-submodules https://github.com/cpp-lln-lab/bidspm.git - -If you just need the code without the commit history download and unzip, -you can fin the latest version from `HERE `_. - Initialization ============== diff --git a/docs/source/usage_notes.rst b/docs/source/usage_notes.rst index a863c39db..e1354fb7c 100644 --- a/docs/source/usage_notes.rst +++ b/docs/source/usage_notes.rst @@ -1,8 +1,8 @@ Usage notes *********** -MATLAB API -========== +API +=== .. module:: src.messages .. autofunction:: bidspmHelp diff --git a/lib/bids-matlab b/lib/bids-matlab index 125f21763..43eb70b8d 160000 --- a/lib/bids-matlab +++ b/lib/bids-matlab @@ -1 +1 @@ -Subproject commit 125f217631b864b8af4fdc0b77f4d769b3286f89 +Subproject commit 43eb70b8d1879f7742de66170edaeb681c1409d5 diff --git a/src/IO/getData.m b/src/IO/getData.m index abf583d40..e582cb66e 100644 --- a/src/IO/getData.m +++ b/src/IO/getData.m @@ -61,7 +61,7 @@ 'verbose', opt.verbosity > 1, ... 'filter', layout_filter); - if strcmp(opt.pipeline.type, 'stats') + if strcmp(opt.pipeline.type, 'stats') && ~opt.pipeline.isBms if exist(fullfile(opt.dir.raw, 'layout.mat'), 'file') == 2 msg = sprintf('Loading BIDS raw layout from:\n\t%s', ... bids.internal.format_path(fullfile(opt.dir.raw, 'layout.mat'))); diff --git a/src/QA/compileScrubbingStats.m b/src/QA/compileScrubbingStats.m index e73cb34b0..1280c54b0 100644 --- a/src/QA/compileScrubbingStats.m +++ b/src/QA/compileScrubbingStats.m @@ -29,6 +29,9 @@ function compileScrubbingStats(statsFolder) json = bids.util.jsondecode(thisFile); output.NumberTimePoints(end + 1) = json.NumberTimePoints; + if isempty(json.ProportionCensored) + json.ProportionCensored = nan; + end output.ProportionCensored(end + 1) = json.ProportionCensored; end diff --git a/src/batches/preproc/setBatchSmoothingFunc.m b/src/batches/preproc/setBatchSmoothingFunc.m index f73d79a19..0e5fb4d79 100644 --- a/src/batches/preproc/setBatchSmoothingFunc.m +++ b/src/batches/preproc/setBatchSmoothingFunc.m @@ -55,6 +55,10 @@ sessions{iSes}, ... runs{iRun}, ... opt); + if isempty(fileName) + continue + end + if isstruct(metadata) metadata = {metadata}; end diff --git a/src/batches/stats/setBatchSubjectLevelGLMSpec.m b/src/batches/stats/setBatchSubjectLevelGLMSpec.m index 400556de4..5545cc9f8 100644 --- a/src/batches/stats/setBatchSubjectLevelGLMSpec.m +++ b/src/batches/stats/setBatchSubjectLevelGLMSpec.m @@ -149,7 +149,8 @@ end end - % When doing model comparison all runs must have same number of confound regressors + % When doing model comparison + % all runs must have same number of confound regressors % so we pad them with zeros if necessary spmSess = orderAndPadCounfoundMatFile(spmSess, opt); @@ -177,7 +178,9 @@ if ~isempty(spmSess(iSpmSess).onsetsFile) onsetsMatToTsv(spmSess(iSpmSess).onsetsFile); end - regressorsMatToTsv(spmSess(iSpmSess).counfoundMatFile); + if ~isempty(spmSess(iSpmSess).counfoundMatFile) + regressorsMatToTsv(spmSess(iSpmSess).counfoundMatFile); + end end if opt.model.designOnly diff --git a/src/bids/getBoldFilename.m b/src/bids/getBoldFilename.m index 94e25fa5c..7d3ea9257 100644 --- a/src/bids/getBoldFilename.m +++ b/src/bids/getBoldFilename.m @@ -45,16 +45,16 @@ sessionID, ... runID, opt.bidsFilterFile.bold.suffix); - % TODO throw an error that says what query actually failed to return a file - % this might need some refacoring to be able to access the query from here even though - % some part of it is in getInfo if isempty(boldFilename) msg = sprintf('No bold file found in:\n\t%s\nfor filter:%s\n', ... bids.internal.format_path(BIDS.pth), ... bids.internal.create_unordered_list(opt.query)); id = 'emptyInput'; - logger('ERROR', msg, 'filename', mfilename(), 'id', id); + logger('WARNING', msg, 'filename', mfilename(), 'id', id); + subFuncDataDir = []; + metadata = []; + return end % in case files have been unzipped, we do it now diff --git a/src/cli/baseInputParser.m b/src/cli/baseInputParser.m index c7771bbd3..cace91f09 100644 --- a/src/cli/baseInputParser.m +++ b/src/cli/baseInputParser.m @@ -23,8 +23,6 @@ addParameter(args, 'action', defaultAction, isChar); addParameter(args, 'participant_label', {}, isCellStr); - addParameter(args, 'task', {}, isCellStr); - addParameter(args, 'space', {}, isCellStr); addParameter(args, 'bids_filter_file', struct([]), isFileOrStruct); addParameter(args, 'verbosity', 2, isPositiveScalar); diff --git a/src/cli/cliBayesModel.m b/src/cli/cliBayesModel.m new file mode 100644 index 000000000..8ff4aee9f --- /dev/null +++ b/src/cli/cliBayesModel.m @@ -0,0 +1,30 @@ +function cliBayesModel(varargin) + % Run stats on bids datasets. + % + % Type ``bidspm help`` for more info. + % + + % TODO make sure that options defined in JSON or passed as a structure + % overrides any other arguments + + % (C) Copyright 2023 bidspm developers + args = inputParserForBayesModel(); + try + parse(args, varargin{:}); + catch ME + displayArguments(varargin{:}); + rethrow(ME); + end + + validate(args); + + opt = getOptionsFromCliArgument(args); + opt.pipeline.type = 'stats'; + opt.pipeline.isBms = true; + opt = checkOptions(opt); + + saveOptions(opt); + + bidsModelSelection(opt, 'action', 'all'); + +end diff --git a/src/cli/cliCopy.m b/src/cli/cliCopy.m index 71c7dbe8a..8727f15e1 100644 --- a/src/cli/cliCopy.m +++ b/src/cli/cliCopy.m @@ -6,7 +6,12 @@ function cliCopy(varargin) % (C) Copyright 2023 bidspm developers args = inputParserForCopy(); - parse(args, varargin{:}); + try + parse(args, varargin{:}); + catch ME + displayArguments(varargin{:}); + rethrow(ME); + end opt = getOptionsFromCliArgument(args); opt.pipeline.type = 'preproc'; diff --git a/src/cli/cliCreateRoi.m b/src/cli/cliCreateRoi.m index f3f1a60f7..ac910e6ad 100644 --- a/src/cli/cliCreateRoi.m +++ b/src/cli/cliCreateRoi.m @@ -6,11 +6,17 @@ function cliCreateRoi(varargin) % (C) Copyright 2023 bidspm developers args = inputParserForCreateRoi(); - parse(args, varargin{:}); + try + parse(args, varargin{:}); + catch ME + displayArguments(varargin{:}); + rethrow(ME); + end opt = getOptionsFromCliArgument(args); opt = checkOptions(opt); opt.roi.space = opt.space; + saveOptions(opt); boilerplate(opt, ... @@ -18,7 +24,7 @@ function cliCreateRoi(varargin) 'pipelineType', 'create_roi', ... 'verbosity', 0); - if opt.boilerplate_only + if opt.boilerplateOnly return end diff --git a/src/cli/cliDefaultModel.m b/src/cli/cliDefaultModel.m index f6a3879b7..18525886a 100644 --- a/src/cli/cliDefaultModel.m +++ b/src/cli/cliDefaultModel.m @@ -6,7 +6,13 @@ function cliDefaultModel(varargin) % (C) Copyright 2023 bidspm developers args = inputParserForCreateModel(); - parse(args, varargin{:}); + try + parse(args, varargin{:}); + catch ME + displayArguments(varargin{:}); + rethrow(ME); + end + opt = getOptionsFromCliArgument(args); if ~isfield(opt, 'taskName') opt.taskName = ''; @@ -14,5 +20,6 @@ function cliDefaultModel(varargin) opt = checkOptions(opt); saveOptions(opt); + createDefaultStatsModel(opt.dir.raw, opt, lower(args.Results.ignore)); end diff --git a/src/cli/cliPreprocess.m b/src/cli/cliPreprocess.m index ef2590384..7c6644e9e 100644 --- a/src/cli/cliPreprocess.m +++ b/src/cli/cliPreprocess.m @@ -9,7 +9,12 @@ function cliPreprocess(varargin) % (C) Copyright 2023 bidspm developers args = inputParserForPreprocess(); - parse(args, varargin{:}); + try + parse(args, varargin{:}); + catch ME + displayArguments(varargin{:}); + rethrow(ME); + end if ~strcmp(args.Results.analysis_level, 'subject') errorHandling(mfilename(), ... @@ -23,6 +28,7 @@ function cliPreprocess(varargin) opt = getOptionsFromCliArgument(args); opt.pipeline.type = 'preproc'; opt = checkOptions(opt); + saveOptions(opt); if ~opt.anatOnly && (isempty(opt.taskName) || numel(opt.taskName) > 1) @@ -37,17 +43,17 @@ function cliPreprocess(varargin) 'outputPath', fullfile(opt.dir.output, 'reports'), ... 'pipelineType', 'preprocess', ... 'verbosity', 0); - if opt.boilerplate_only + if opt.boilerplateOnly return end bidsCopyInputFolder(opt); - if opt.dummy_scans > 0 + if opt.dummyScans > 0 tmpOpt = opt; tmpOpt.dir.input = tmpOpt.dir.preproc; bidsRemoveDummies(tmpOpt, ... - 'dummyScans', tmpOpt.dummy_scans, ... + 'dummyScans', tmpOpt.dummyScans, ... 'force', false); end diff --git a/src/cli/cliSmooth.m b/src/cli/cliSmooth.m index 5f9ec473e..e9a27b9a3 100644 --- a/src/cli/cliSmooth.m +++ b/src/cli/cliSmooth.m @@ -9,7 +9,12 @@ function cliSmooth(varargin) % TODO make sure that options defined in JSON or passed as a structure % overrides any other arguments args = inputParserForSmooth(); - parse(args, varargin{:}); + try + parse(args, varargin{:}); + catch ME + displayArguments(varargin{:}); + rethrow(ME); + end opt = getOptionsFromCliArgument(args); opt.pipeline.type = 'preproc'; diff --git a/src/cli/cliStats.m b/src/cli/cliStats.m index 890734d50..0a20a9b33 100644 --- a/src/cli/cliStats.m +++ b/src/cli/cliStats.m @@ -13,87 +13,111 @@ function cliStats(varargin) validate(args); - opt = getOptionsFromCliArgument(args); - if opt.glm.roibased.do - opt.bidsFilterFile.roi.space = opt.space; - opt.bidsFilterFile.roi.label = opt.roi.name; - end - opt.pipeline.type = 'stats'; - opt = checkOptions(opt); - action = args.Results.action; analysisLevel = args.Results.analysis_level; nodeName = args.Results.node_name; concatenate = args.Results.concatenate; isSubjectLevel = strcmp(analysisLevel, 'subject'); + specify_only = strcmp(action, 'specify_only'); estimate = strcmp(action, 'stats'); contrasts = ismember(action, {'stats', 'contrasts'}); results = ismember(action, {'stats', 'contrasts', 'results'}); + if specify_only + estimate = false; + end + + opt = getOptionsFromCliArgument(args); + + if opt.glm.roibased.do + opt.bidsFilterFile.roi.space = opt.space; + opt.bidsFilterFile.roi.label = opt.roi.name; + end + + opt.pipeline.type = 'stats'; + if opt.model.designOnly contrasts = false; results = false; end - saveOptions(opt); + allModels = cellstr(opt.model.file); - boilerplate(opt, ... - 'outputPath', fullfile(opt.dir.output, 'reports'), ... - 'pipelineType', 'stats', ... - 'verbosity', 0); - if opt.boilerplate_only - return - end - - if opt.glm.roibased.do + for iModel = 1:numel(allModels) - bidsFFX('specify', opt); - if ~opt.model.designOnly - bidsRoiBasedGLM(opt); + if isfield(opt, 'model') + opt = rmfield(opt, 'model'); end + opt.model.file = allModels{iModel}; - else + opt = checkOptions(opt); - if estimate + saveOptions(opt); - if isSubjectLevel - if opt.model.designOnly - bidsFFX('specify', opt); - compileScrubbingStats(opt.dir.stats); - else - bidsFFX('specifyAndEstimate', opt); - compileScrubbingStats(opt.dir.stats); - end + boilerplate(opt, ... + 'outputPath', fullfile(opt.dir.output, 'reports'), ... + 'pipelineType', 'stats', ... + 'verbosity', 0); + if opt.boilerplateOnly + continue + end - else - if opt.fwhm.contrast > 0 - bidsSmoothContrasts(opt); - end - bidsRFX('RFX', opt, 'nodeName', nodeName); + if opt.glm.roibased.do + bidsFFX('specify', opt); + if ~opt.model.designOnly + bidsRoiBasedGLM(opt); + end + + else + if isSubjectLevel && ... + (specify_only || ... + estimate && opt.model.designOnly) + bidsFFX('specify', opt); + compileScrubbingStats(opt.dir.stats); end - end + if estimate + runEstimate(isSubjectLevel, opt, nodeName); + end - if contrasts - if isSubjectLevel - bidsFFX('contrasts', opt); - else - bidsRFX('contrasts', opt, 'nodeName', nodeName); + if contrasts + runContrasts(isSubjectLevel, opt, nodeName, concatenate); end - if isSubjectLevel && concatenate - bidsConcatBetaTmaps(opt); + if results + bidsResults(opt, ... + 'nodeName', nodeName, ... + 'analysisLevel', analysisLevel); end end - if results - bidsResults(opt, 'nodeName', nodeName, ... - 'analysisLevel', analysisLevel); + end + +end + +function runEstimate(isSubjectLevel, opt, nodeName) + if isSubjectLevel + bidsFFX('specifyAndEstimate', opt); + compileScrubbingStats(opt.dir.stats); + else + if opt.fwhm.contrast > 0 + bidsSmoothContrasts(opt); end + bidsRFX('RFX', opt, 'nodeName', nodeName); + end +end +function runContrasts(isSubjectLevel, opt, nodeName, concatenate) + if isSubjectLevel + bidsFFX('contrasts', opt); + else + bidsRFX('contrasts', opt, 'nodeName', nodeName); end + if isSubjectLevel && concatenate + bidsConcatBetaTmaps(opt); + end end diff --git a/src/cli/getOptionsFromCliArgument.m b/src/cli/getOptionsFromCliArgument.m index 089290308..c7528d745 100644 --- a/src/cli/getOptionsFromCliArgument.m +++ b/src/cli/getOptionsFromCliArgument.m @@ -27,10 +27,6 @@ opt.subjects = args.Results.participant_label; end - if ~isempty(args.Results.task) - opt.taskName = args.Results.task; - end - if ~isempty(args.Results.bids_filter_file) % TODO read from JSON if necessary % TODO validate @@ -39,8 +35,12 @@ opt = overrideSpace(opt, args); + if isfield(args.Results, 'task') + opt.taskName = args.Results.task; + end + if isfield(args.Results, 'boilerplate_only') - opt.boilerplate_only = args.Results.boilerplate_only; + opt.boilerplateOnly = args.Results.boilerplate_only; end if isfield(args.Results, 'dry_run') @@ -56,7 +56,27 @@ opt.query.modality = {'anat'}; end - % preproc + opt = optionsPreprocessing(opt, args, action); + + if ismember(lower(action), {'create_roi', ... + 'stats', ... + 'contrasts', ... + 'results', ... + 'specify_only'}) + opt.dir.preproc = args.Results.preproc_dir; + end + + opt = roiOptions(opt, args); + + opt = optionsStats(opt, args, action); + + if ismember(lower(action), {'bms'}) + opt.toolbox.MACS.model.dir = args.Results.models_dir; + end + +end + +function opt = optionsPreprocessing(opt, args, action) if ismember(lower(action), {'preprocess'}) if ismember('slicetiming', args.Results.ignore) @@ -74,29 +94,39 @@ opt.QA.glm.do = false; end - opt.dummy_scans = args.Results.dummy_scans; + opt.dummyScans = args.Results.dummy_scans; opt.anatOnly = args.Results.anat_only; end +end - if ismember(lower(action), {'create_roi', 'stats', 'contrasts', 'results'}) +function opt = optionsStats(opt, args, action) + if ismember(lower(action), {'stats', ... + 'contrasts', ... + 'results', ... + 'specify_only'}) opt.dir.preproc = args.Results.preproc_dir; - end - opt = roiOptions(opt, args); - - % stats - if ismember(lower(action), {'stats', 'contrasts', 'results'}) - opt.dir.preproc = args.Results.preproc_dir; + % can only be a struct, file or dir opt.model.file = args.Results.model_file; + if ~isstruct(opt.model.file) + if isdir(opt.model.file) + opt.model.file = spm_select('FPList', ... + opt.model.file, ... + '.*_smdl.json'); + end + end + opt.model.designOnly = args.Results.design_only; + opt.glm.keepResiduals = args.Results.keep_residuals; + opt.glm.useDummyRegressor = args.Results.use_dummy_regressor; + opt = overrideRoiBased(opt, args); end - end function value = bidsAppsActions() @@ -108,7 +138,9 @@ 'default_model'; ... 'stats'; ... 'contrasts'; ... - 'results'}; + 'results'; ... + 'specify_only'; ... + 'bms'}; end function opt = getOptions(args) @@ -169,6 +201,9 @@ end function opt = overrideSpace(opt, args) + if ~isfield(args.Results, 'space') + return + end if ~isempty(args.Results.space) if isfield(opt, 'space') && ~all(ismember(args.Results.space, opt.space)) overrideMsg('space', convertToString(args.Results.space), ... diff --git a/src/cli/inputParserForBayesModel.m b/src/cli/inputParserForBayesModel.m new file mode 100644 index 000000000..269e5f2ff --- /dev/null +++ b/src/cli/inputParserForBayesModel.m @@ -0,0 +1,21 @@ +function args = inputParserForBayesModel() + % Returns an input parser for cliSBayesModel + % + % Type ``bidspm help`` for more info. + % + + % (C) Copyright 2022 bidspm developers + args = baseInputParser(); + + isLogical = @(x) islogical(x) && numel(x) == 1; + isPositiveScalar = @(x) isnumeric(x) && numel(x) == 1 && x >= 0; + isFolder = @(x) isdir(x); + isEmptyOrCellstr = @(x) isempty(x) || iscellstr(x); %#ok<*ISCLSTR> + + addParameter(args, 'models_dir', pwd, isFolder); + + addParameter(args, 'fwhm', 6, isPositiveScalar); + addParameter(args, 'dry_run', false, isLogical); + addParameter(args, 'skip_validation', false, isLogical); + +end diff --git a/src/cli/inputParserForCopy.m b/src/cli/inputParserForCopy.m index 1aa7c9a7a..13c2daf95 100644 --- a/src/cli/inputParserForCopy.m +++ b/src/cli/inputParserForCopy.m @@ -6,12 +6,16 @@ % (C) Copyright 2023 bidspm developers isLogical = @(x) islogical(x) && numel(x) == 1; + isCharOrCellstr = @(x) ischar(x) || iscellstr(x); %#ok<*ISCLSTR> + isCellStr = @(x) iscellstr(x); args = baseInputParser(); % allow unmatched in case this is called from cliSmooth args.KeepUnmatched = true; + addParameter(args, 'space', {}, isCellStr); + addParameter(args, 'task', {}, isCharOrCellstr); addParameter(args, 'force', false, isLogical); addParameter(args, 'anat_only', false, isLogical); end diff --git a/src/cli/inputParserForCreateModel.m b/src/cli/inputParserForCreateModel.m index 9a9467a67..c3a8b64b2 100644 --- a/src/cli/inputParserForCreateModel.m +++ b/src/cli/inputParserForCreateModel.m @@ -8,9 +8,10 @@ args = baseInputParser(); isEmptyOrCellstr = @(x) isempty(x) || iscellstr(x); %#ok<*ISCLSTR> + isCellStr = @(x) iscellstr(x); - % realying on the base parser adds too many options - % like task + addParameter(args, 'space', {}, isCellStr); + addParameter(args, 'task', {}, isEmptyOrCellstr); % :param ignore: Optional. Cell string that can contain: % - ``"Transformations"`` diff --git a/src/cli/inputParserForCreateRoi.m b/src/cli/inputParserForCreateRoi.m index 289f28c31..597a75697 100644 --- a/src/cli/inputParserForCreateRoi.m +++ b/src/cli/inputParserForCreateRoi.m @@ -16,6 +16,7 @@ isCellStr = @(x) iscellstr(x); isLogical = @(x) islogical(x) && numel(x) == 1; + addParameter(args, 'space', {}, isCellStr); addParameter(args, 'roi_dir', '', isDir); addParameter(args, 'preproc_dir', '', isDir); addParameter(args, 'hemisphere', {'L', 'R'}, isCellStr); diff --git a/src/cli/inputParserForPreprocess.m b/src/cli/inputParserForPreprocess.m index 325a63447..2901fcdc2 100644 --- a/src/cli/inputParserForPreprocess.m +++ b/src/cli/inputParserForPreprocess.m @@ -10,7 +10,11 @@ isPositiveScalar = @(x) isnumeric(x) && numel(x) == 1 && x >= 0; isLogical = @(x) islogical(x) && numel(x) == 1; isEmptyOrCellstr = @(x) isempty(x) || iscellstr(x); %#ok<*ISCLSTR> + isEmptyOrisChar = @(x) isempty(x) || ischar(x); %#ok<*ISCLSTR> + isCellStr = @(x) iscellstr(x); + addParameter(args, 'space', {}, isCellStr); + addParameter(args, 'task', '', isEmptyOrisChar); addParameter(args, 'fwhm', 6, isPositiveScalar); addParameter(args, 'dry_run', false, isLogical); addParameter(args, 'anat_only', false, isLogical); diff --git a/src/cli/inputParserForStats.m b/src/cli/inputParserForStats.m index fe9f50e86..252341399 100644 --- a/src/cli/inputParserForStats.m +++ b/src/cli/inputParserForStats.m @@ -8,7 +8,7 @@ args = baseInputParser(); isInAvailableAtlas = @(x) (ischar(x) && ismember(x, supportedAtlases())); - isFileOrStruct = @(x) isstruct(x) || exist(x, 'file') == 2; + isFileOrStructOrIsDir = @(x) isstruct(x) || exist(x, 'file') == 2 || isdir(x); isLogical = @(x) islogical(x) && numel(x) == 1; isChar = @(x) ischar(x); isPositiveScalar = @(x) isnumeric(x) && numel(x) == 1 && x >= 0; @@ -16,8 +16,10 @@ isCellStr = @(x) iscellstr(x); isEmptyOrCellstr = @(x) isempty(x) || iscellstr(x); %#ok<*ISCLSTR> + addParameter(args, 'space', {}, isCellStr); + addParameter(args, 'task', {}, isCellStr); addParameter(args, 'preproc_dir', pwd, isFolder); - addParameter(args, 'model_file', struct([]), isFileOrStruct); + addParameter(args, 'model_file', struct([]), isFileOrStructOrIsDir); addParameter(args, 'fwhm', 6, isPositiveScalar); addParameter(args, 'dry_run', false, isLogical); @@ -27,6 +29,7 @@ addParameter(args, 'design_only', false, isLogical); addParameter(args, 'concatenate', false, isLogical); addParameter(args, 'keep_residuals', false, isLogical); + addParameter(args, 'use_dummy_regressor', false, isLogical); addParameter(args, 'roi_atlas', 'neuromorphometrics', isInAvailableAtlas); diff --git a/src/defaults/checkOptions.m b/src/defaults/checkOptions.m index 2200bf4f7..7e4995832 100644 --- a/src/defaults/checkOptions.m +++ b/src/defaults/checkOptions.m @@ -62,14 +62,17 @@ % - ``opt.pipeline.type = 'preproc'`` - % Switch it to ``stats`` when running GLMs. % - ``opt.pipeline.name`` + % - ``opt.pipeline.isBms`` whether this is a bayesion model selection + % pipeline % - % - ``opt.boilerplate_only = false`` - + % - ``opt.boilerplateOnly = false`` - % If set to ``true`` only creates dataset description reports and methods description. % Overwrites previous versions. % % - ``opt.zeropad = 2`` - % Number of zeros used for padding subject numbers, - % in case subjects should be fetched by their index ``1`` and not their label ``O1'``. + % in case subjects should be fetched by their index ``1`` + % and not their label ``O1'``. % % - ``opt.rename.do = true`` - % Set to ``false`` to skip renaming files with ``bidsRename()``. @@ -97,7 +100,8 @@ % Set to ``true`` to only preprocess the anatomical file. % % - ``opt.segment.force = false`` - - % Set to ``true`` to ignore previous output of the segmentation and force to run it again + % Set to ``true`` to ignore previous output of the segmentation + % and force to run it again % % - ``opt.skullstrip.mean = false`` - % Set to ``true`` to skulstrip mean functional image @@ -109,7 +113,6 @@ % - ``opt.skullstrip.do = true`` - % Set to ``true`` to skip skullstripping. % - % % - ``opt.stc.skip = false`` - % Boolean flag to skip slice time correction or not. % - ``opt.stc.referenceSlice = []`` - @@ -119,11 +122,17 @@ % - ``opt.funcVoxelDims = []`` - % Voxel dimensions to use for resampling of functional data at normalization. % + % % - **STATISTICS OPTIONS** % % - ``opt.model.file = ''`` - % Path to the BIDS model file that contains the model % to specify and the contrasts to compute. + % A path to a dir can be passed as well. + % In this case all *_smdl.json files will be used + % and looped over. + % This can useful to specify several models at once + % Before running Bayesion model selection on them. % % - ``opt.fwhm.contrast = 0`` - % FWHM to apply to the contrast images before bringing them at the group level. @@ -134,15 +143,12 @@ % % - ``opt.glm.roibased.do = false`` - % Set to ``true`` to use the ``bidsRoiBasedGLM`` workflow. - % % - ``opt.glm.useDummyRegressor = false`` - % Set to ``true`` to add dummy regressors when a condition is missing from a run. % See ``bidsModelSelection()`` for more information. - % % - ``opt.glm.maxNbVols = Inf`` - % Sets the maximum number of volumes to include in a run in a subject level GLM. % This can be useful if some time series have more volumes than necessary. - % % - ``opt.glm.keepResiduals = false`` - % Keep the subject level GLM residuals if set to ``true``. % @@ -256,8 +262,9 @@ fieldsToSet.pipeline.type = ''; fieldsToSet.pipeline.name = 'bidspm'; + fieldsToSet.pipeline.isBms = false; - fieldsToSet.boilerplate_only = false; + fieldsToSet.boilerplateOnly = false; fieldsToSet.useBidsSchema = false; @@ -284,7 +291,7 @@ %% General options for functional data fieldsToSet.funcVolToSelect = []; - fieldsToSet.dummy_scans = 0; + fieldsToSet.dummyScans = 0; %% Options for slice time correction % all in seconds @@ -334,7 +341,9 @@ function checkFields(opt) - if isfield(opt, 'taskName') && isempty(opt.taskName) + if isfield(opt, 'taskName') && ... + isempty(opt.taskName) && ... + ~strcmp(opt.pipeline.type, 'stats') msg = 'You may need to provide the name of the task to analyze.'; id = 'noTask'; diff --git a/src/defaults/getOptionsFromModel.m b/src/defaults/getOptionsFromModel.m index f7b0be93c..565349d16 100644 --- a/src/defaults/getOptionsFromModel.m +++ b/src/defaults/getOptionsFromModel.m @@ -10,17 +10,30 @@ % TODO refactor to pass everything to bids filter file instead of query? % TODO override content in bids_filter_file - if ~strcmpi(opt.pipeline.type, 'stats') + if ~ismember(opt.pipeline.type, {'stats', 'bms'}) return end - if isempty(opt.model.file) || exist(opt.model.file, 'file') ~= 2 + if opt.pipeline.isBms + files = spm_select('FPList', ... + opt.toolbox.MACS.model.dir, ... + '.*_smdl.json'); + opt.toolbox.MACS.model.files = cellstr(files); + return + end + + modelIsSingleFile = size(opt.model.file, 1) == 1; + + if isempty(opt.model.file) || ... + (modelIsSingleFile && exist(opt.model.file, 'file') ~= 2) msg = sprintf('model file does not exist:\n %s', opt.model.file); id = 'modelFileMissing'; logger('ERROR', msg, 'filename', mfilename(), 'id', id); end - if ~isfield(opt.model, 'bm') || isempty(opt.model.bm) + modelNotLoaded = ~isfield(opt.model, 'bm') || isempty(opt.model.bm); + + if modelIsSingleFile && modelNotLoaded opt.model.bm = BidsModel('file', opt.model.file, ... 'tolerant', opt.tolerant, ... 'verbose', opt.verbosity > 1); @@ -89,7 +102,7 @@ case cat(1, fromQuery(), {'space'}) case {'task', 'sub', 'space'} - if isfield(opt, targetField) + if isfield(opt, targetField) && ~isempty(opt.(targetField)) overrideWarning(opt.(targetField), thisEntity, inputsAlreadyInOptions, opt); end diff --git a/src/messages/bidspmHelp.m b/src/messages/bidspmHelp.m index e371bb2d3..ee882c2b6 100644 --- a/src/messages/bidspmHelp.m +++ b/src/messages/bidspmHelp.m @@ -17,13 +17,12 @@ function bidspmHelp() % .. code-block:: matlab % % bidspm(bids_dir, output_dir, analysis_level, ... - % 'action', 'some_action', ... - % 'participant_label', {}, ... - % 'task', {}, ... - % 'space', {'individual', 'IXI549Space'}, ... - % 'bids_filter_file', struct([]), ... - % 'verbosity', 2, ... - % 'options', struct([])) + % 'action', 'some_action', ... + % 'participant_label', {}, ... + % 'space', {'individual', 'IXI549Space'}, ... + % 'bids_filter_file', struct([]), ... + % 'verbosity', 2, ... + % 'options', struct([])) % % % *Obligatory parameters* @@ -41,16 +40,18 @@ function bidspmHelp() % :type action: char % :param action: defines the pipeline to run; can be any of: % - % - ``'copy'``: copies fmriprep data for smoothing - % - ``'preprocess'``: preprocesses data - % - ``'smooth'``: smooths data + % - ``'copy'``: copies fmriprep data for smoothing + % - ``'preprocess'``: preprocesses data + % - ``'smooth'``: smooths data % - ``'default_model'``: creates a default BIDS stats model - % - ``'create_roi'``: creates ROIs from a given atlas - % - ``'stats'``: runs model specification / estimation, contrast computation, display results - % - ``'contrasts'``: runs contrast computation, display results - % - ``'results'``: displays results + % - ``'create_roi'``: creates ROIs from a given atlas + % - ``'stats'``: runs model specification / estimation, + % contrast computation, display results + % - ``'contrasts'``: runs contrast computation, display results + % - ``'results'``: displays results + % - ``'bms'``: performs bayesian model selection + % - ``'specify_only'`` only specifies the models % - % *Optional parameters common to all actions* % % :param participant_label: cell of participants labels. % For example: ``{'01', '03', '08'}``. @@ -58,16 +59,14 @@ function bidspmHelp() % Defaults to ``{}`` % :type participant_label: cellstr % - % :param task: Defaults to ``{}`` - % :type task: cell string - % % :param space: Defaults to ``{}`` % :type space: cell string % % :param bids_filter_file: path to JSON file or structure % :type bids_filter_file: path % - % :param verbosity: can be any value between ``0`` and ``3``. Defaults to ``2`` + % :param verbosity: can be any value between ``0`` and ``3``. + % Defaults to ``2`` % :type verbosity: positive integer % % :param options: See the ``checkOptions`` help to see the available options. @@ -86,28 +85,33 @@ function bidspmHelp() % .. code-block:: matlab % % bidspm(bids_dir, output_dir, 'subject', ... - % 'action', 'preprocess', ... - % 'participant_label', {}, ... - % 'task', {}, ... - % 'space', {'individual', 'IXI549Space'}, ... - % 'bids_filter_file', struct([]), ... - % 'verbosity', 2, ... - % 'options', struct([]), ... - % 'boilerplate_only', false, ... - % 'dry_run', false, ... - % 'dummy_scans', 0, ... - % 'anat_only', false, ... - % 'ignore', {}, ... - % 'fwhm', 6, ... - % 'skip_validation', false) - % - % - % *Extra parameters for preprocessing* + % 'action', 'preprocess', ... + % 'participant_label', {}, ... + % 'task', '', ... + % 'space', {'individual', 'IXI549Space'}, ... + % 'bids_filter_file', struct([]), ... + % 'verbosity', 2, ... + % 'options', struct([]), ... + % 'boilerplate_only', false, ... + % 'dry_run', false, ... + % 'dummy_scans', 0, ... + % 'anat_only', false, ... + % 'ignore', {}, ... + % 'fwhm', 6, ... + % 'skip_validation', false) + % % % :param boilerplate_only: Only creates dataset description reports. % and methods description. Defaults to ``false``. % :type boilerplate_only: logical % + % :param space: Defaults to ``{}`` + % :type space: cell string + % + % :param task: Only a single task can be processed at once. + % Defaults to ``''``. + % :type task: char + % % :param dry_run: Defaults to ``false`` % :type dry_run: logical % @@ -118,7 +122,7 @@ function bidspmHelp() % :type anat_only: logical % % :param ignore: can be any of ``{'fieldmaps', 'slicetiming', 'unwarp', 'qa'}`` - % :type ignore: cell string + % :type ignore: cellstr % % :param fwhm: Smoothing to apply to the preprocessed data. Defaults to ``6``. % :type fwhm: positive scalar @@ -137,18 +141,22 @@ function bidspmHelp() % .. code-block:: matlab % % bidspm(bids_dir, output_dir, 'subject', ... - % 'action', 'copy', ... - % 'participant_label', {}, ... - % 'task', {}, ... - % 'space', {'individual', 'IXI549Space'}, ... - % 'bids_filter_file', struct([]), ... - % 'verbosity', 2, ... - % 'options', struct([]), ... - % 'anat_only', false, ... - % 'force', false) + % 'action', 'copy', ... + % 'participant_label', {}, ... + % 'task', {}, ... + % 'space', {'individual', 'IXI549Space'}, ... + % 'bids_filter_file', struct([]), ... + % 'verbosity', 2, ... + % 'options', struct([]), ... + % 'anat_only', false, ... + % 'force', false) % % - % *Extra parameters for copy* + % :param space: Defaults to ``{}`` + % :type space: cell string + % + % :param task: Defaults to ``{}`` + % :type task: char or cell string % % :param force: Overwrites previous data if true. Defaults to ``false``. % :type force: logical @@ -165,16 +173,19 @@ function bidspmHelp() % .. code-block:: matlab % % bidspm(bids_dir, output_dir, 'subject', ... - % 'action', 'create_roi', ... - % 'participant_label', {}, ... - % 'space', {'MNI'}, ... - % 'bids_filter_file', struct([]), ... - % 'preproc_dir', preproc_dir, ... - % 'verbosity', 2, ... - % 'options', struct([]), ... - % 'roi_atlas', 'wang', ... - % 'roi_name', {'V1v', 'V1d'}, ... - % 'hemisphere', {'L', 'R'}) + % 'action', 'create_roi', ... + % 'participant_label', {}, ... + % 'space', {'MNI'}, ... + % 'bids_filter_file', struct([]), ... + % 'preproc_dir', preproc_dir, ... + % 'verbosity', 2, ... + % 'options', struct([]), ... + % 'roi_atlas', 'wang', ... + % 'roi_name', {'V1v', 'V1d'}, ... + % 'hemisphere', {'L', 'R'}) + % + % :param space: Defaults to ``{}`` + % :type space: cell string % % :param roi_atlas: Can be any of: % - ``'visfatlas'`` @@ -208,15 +219,21 @@ function bidspmHelp() % .. code-block:: matlab % % bidspm(bids_dir, output_dir, 'subject', ... - % 'action', 'smooth', ... - % 'participant_label', {}, ... - % 'task', {}, ... - % 'space', {'individual', 'IXI549Space'}, ... - % 'bids_filter_file', struct([]), ... - % 'options', struct([]), ... - % 'verbosity', 2, ... - % 'fwhm', 6, ... - % 'dry_run', false) + % 'action', 'smooth', ... + % 'participant_label', {}, ... + % 'task', {}, ... + % 'space', {'individual', 'IXI549Space'}, ... + % 'bids_filter_file', struct([]), ... + % 'options', struct([]), ... + % 'verbosity', 2, ... + % 'fwhm', 6, ... + % 'dry_run', false) + % + % :param space: Defaults to ``{}`` + % :type space: cell string + % + % :param task: Defaults to ``{}`` + % :type task: char or cell string % % :param fwhm: Smoothing to apply to the preprocessed data. Defaults to ``6``. % :type fwhm: positive scalar @@ -236,15 +253,21 @@ function bidspmHelp() % .. code-block:: matlab % % bidspm(bids_dir, output_dir, 'dataset', ... - % 'action', 'default_model', ... - % 'participant_label', {}, ... - % 'task', {}, ... - % 'space', {'individual', 'IXI549Space'}, ... - % 'bids_filter_file', struct([]), ... - % 'verbosity', 2, ... - % 'options', struct([]), ... - % 'ignore', {}) + % 'action', 'default_model', ... + % 'participant_label', {}, ... + % 'task', {}, ... + % 'space', {'individual', 'IXI549Space'}, ... + % 'bids_filter_file', struct([]), ... + % 'verbosity', 2, ... + % 'options', struct([]), ... + % 'ignore', {}) + % % + % :param space: Defaults to ``{}`` + % :type space: cell string + % + % :param task: Defaults to ``{}`` + % :type task: char or cell string % % :param ignore: can be any of ``{'contrasts', 'transformations', 'dataset'}`` % :type ignore: cell string @@ -255,33 +278,36 @@ function bidspmHelp() % % .. note:: % - % - ``'stats'`` runs model specification / estimation, contrast computation, display results - % - ``'contrasts'`` runs contrast computation, display results - % - ``'results'`` displays results + % - ``'stats'`` runs model specification / estimation, + % contrast computation, display results + % - ``'contrasts'`` runs contrast computation, display results + % - ``'results'`` displays results + % - ``'specify_only'`` only specifies the models % % .. code-block:: matlab % % bidspm(bids_dir, output_dir, 'subject', ... - % 'action', 'stats', ... - % 'participant_label', {}, ... - % 'task', {}, ... - % 'space', {'individual', 'IXI549Space'}, ... - % 'bids_filter_file', struct([]), ... - % 'options', struct([]), ..., - % 'verbosity', 2, ... - % 'preproc_dir', preproc_dir, ... - % 'model_file', model_file, ... % specific to stats - % 'fwhm', 6, ... - % 'dry_run', false, ... - % 'boilerplate_only', false, ... - % 'roi_atlas', 'neuromorphometrics', ... - % 'roi_based', false, ... - % 'roi_dir', '', ... - % 'roi_name', {''}, ... - % 'design_only', false, ... - % 'ignore', {}, ... - % 'concatenate', false, ... - % 'skip_validation', false) + % 'action', 'stats', ... + % 'participant_label', {}, ... + % 'task', {}, ... + % 'space', {'individual', 'IXI549Space'}, ... + % 'bids_filter_file', struct([]), ... + % 'options', struct([]), ..., + % 'verbosity', 2, ... + % 'preproc_dir', preproc_dir, ... + % 'model_file', model_file, ... % specific to stats + % 'fwhm', 6, ... + % 'dry_run', false, ... + % 'boilerplate_only', false, ... + % 'roi_atlas', 'neuromorphometrics', ... + % 'roi_based', false, ... + % 'roi_dir', '', ... + % 'roi_name', {''}, ... + % 'design_only', false, ... + % 'ignore', {}, ... + % 'concatenate', false, ... + % 'use_dummy_regressor', false) + % 'skip_validation', false) % % % *Obligatory parameters* @@ -289,21 +315,29 @@ function bidspmHelp() % :param preproc_dir: path to preprocessed data % :type preproc_dir: path % - % :param model_file: - % :type model_file: path to JSON file or structure + % :param model_file: Path to the BIDS model file that contains the model + % to specify and the contrasts to compute. + % A path to a dir can be passed as well. + % In this case all *_smdl.json files will be used + % and looped over. + % This can useful to specify several models at once + % Before running Bayesion model selection on them. + % :type model_file: path to JSON file or dir or structure + % + % :param space: Defaults to ``{}`` + % :type space: cell string % % % *Optional parameters* % - % :param fwhm: smoothing lelvel of the preprocessed data + % :param fwhm: smoothing level of the preprocessed data % :type fwhm: positive scalar % - % :param design_only: to only run the model specification when at the group level + % :param design_only: to only run the model specification % :type design_only: logical % % :param ignore: can be any of ``{'qa'}``, to skip - % quality controls or contanetation of beta images - % into a single 4D image. + % quality controls into a single 4D image. % :type ignore: cell string % % :param concatenate: will contatenate the beta images of the @@ -316,19 +350,68 @@ function bidspmHelp() % :param roi_atlas: Name of the atlas to use to label activations in MNI space. % :type roi_atlas: char % - % :param roi_based: + % :param roi_based: Set to ``true`` to run a ROI-based analysis. + % Defaults to ``false``. % :type roi_based: logical % - % :param roi_dir: + % :param roi_dir: Path to the directory containing the ROIs. % :type roi_dir: path % - % :param roi_name: + % :param roi_name: Names or regex expression of the ROI to use. % :type roi_name: cell string % - % :param boilerplate_only: Only creates dataset description reports. - % and methods description. Defaults to ``false``. - % :type boilerplate_only: logical + % :param boilerplate_only: Only creates dataset description reports. + % and methods description. Defaults to ``false``. + % :type boilerplate_only: logical + % + % :param use_dummy_regressor: If true any missing condition will be modelled + % by a dummy regressor of ``NaN``. + % Defaults to ``false``. + % :type use_dummy_regressor: logical + % + % + % + % **BAYESIAN MODE SELECTION** + % + % .. code-block:: matlab + % + % bidspm(bids_dir, output_dir, 'subject', ... + % 'action', 'bms', ... + % 'participant_label', {}, ... + % 'options', struct([]), ..., + % 'verbosity', 2, ... + % 'models_dir', models_dir, ... + % 'fwhm', 6, ... + % 'dry_run', false, ... + % 'skip_validation', false) + % + % :param models_dir: A path to a dir can be passed as well. + % In this case all ``*_smdl.json`` files will be used + % and looped over. + % + % :param dry_run: Defaults to ``false``. + % :type dry_run: logical + % + % :param fwhm: smoothing level of the preprocessed data + % :type fwhm: positive scalar + % + % .. note:: + % + % For the bayesian model selection to function + % you must first specify all your models using the ``'specify_only'`` action + % with the options ``'use_dummy_regressor', true``. + % + % .. code-block:: matlab + % + % opt.glm.useDummyRegressor = true; % + % bidspm(bids_dir, output_dir, 'subject', ... + % 'participant_label', participant_label, ... + % 'preproc_dir', preproc_dir, ... + % 'action', 'specify_only', ... + % 'model_file', models_dir, ... + % 'use_dummy_regressor', true + % 'fwhm', FWHM); % % % **low level calls** diff --git a/src/reports/boilerplate.m b/src/reports/boilerplate.m index ec55fab24..7a969d328 100644 --- a/src/reports/boilerplate.m +++ b/src/reports/boilerplate.m @@ -80,10 +80,10 @@ case 'preprocess' - if opt.dummy_scans == 0 - opt.dummy_scans = false; + if opt.dummyScans == 0 + opt.dummyScans = false; else - opt.dummy_scans = struct('nb', opt.dummy_scans); + opt.dummyScans = struct('nb', opt.dummyScans); end opt.normalization = false; diff --git a/src/stats/subject_level/createAndReturnCounfoundMatFile.m b/src/stats/subject_level/createAndReturnCounfoundMatFile.m index 23a216941..15938e3f9 100644 --- a/src/stats/subject_level/createAndReturnCounfoundMatFile.m +++ b/src/stats/subject_level/createAndReturnCounfoundMatFile.m @@ -61,6 +61,11 @@ ffxDir = getFFXdir(bf.entities.sub, opt); counfoundFile = fullfile(ffxDir, bf.filename); + if isempty(names) || isempty(R) + counfoundFile = ''; + counfoundMeta = ''; + return + end save(counfoundFile, ... 'names', 'R', ... '-v7'); diff --git a/src/stats/subject_level/orderAndPadCounfoundMatFile.m b/src/stats/subject_level/orderAndPadCounfoundMatFile.m index 317ed5111..b0213c665 100644 --- a/src/stats/subject_level/orderAndPadCounfoundMatFile.m +++ b/src/stats/subject_level/orderAndPadCounfoundMatFile.m @@ -51,6 +51,10 @@ end for iRun = 1:numel(matFiles) + fileToLoad = matFiles{iRun}; + if isempty(fileToLoad) + continue + end load(matFiles{iRun}, 'names'); allConfoundsNames = cat(1, allConfoundsNames, names); %#ok end @@ -58,6 +62,9 @@ for iRun = 1:numel(matFiles) fileToLoad = matFiles{iRun}; + if isempty(fileToLoad) + continue + end load(fileToLoad, 'names', 'R'); [names, R] = reorderCounfounds(names, R, allConfoundsNames); diff --git a/src/utils/displayArguments.m b/src/utils/displayArguments.m new file mode 100644 index 000000000..4c6971827 --- /dev/null +++ b/src/utils/displayArguments.m @@ -0,0 +1,9 @@ +function displayArguments(varargin) + % (C) Copyright 2023 bidspm developers + disp('arguments passed were :'); + for i = 1:numel(varargin) + fprintf('- '); + disp(varargin{i}); + end + fprintf(1, '\n'); +end diff --git a/src/utils/setUpWorkflow.m b/src/utils/setUpWorkflow.m index 6b9f3d57f..2ec68af4b 100644 --- a/src/utils/setUpWorkflow.m +++ b/src/utils/setUpWorkflow.m @@ -57,7 +57,8 @@ [BIDS, opt] = getData(opt, bidsDir); end - if strcmp(opt.pipeline.type, 'stats') && isempty(opt.model.file) + if strcmp(opt.pipeline.type, 'stats') && ... + (isempty(opt.model.file) && ~opt.pipeline.isBms) opt = createDefaultStatsModel(BIDS, opt); end diff --git a/src/workflows/bidsCopyInputFolder.m b/src/workflows/bidsCopyInputFolder.m index 6d74d20ee..3fda568b1 100644 --- a/src/workflows/bidsCopyInputFolder.m +++ b/src/workflows/bidsCopyInputFolder.m @@ -66,6 +66,9 @@ function bidsCopyInputFolder(varargin) if strcmp(filter.modality, 'func') filter.task = opt.taskName; + if isempty(filter.task) + filter.task = bids.query(BIDS, 'tasks', filter); + end end pipeline_name = opt.pipeline.name; diff --git a/src/workflows/bidsReport.m b/src/workflows/bidsReport.m index 818c0e4ba..143df54b5 100644 --- a/src/workflows/bidsReport.m +++ b/src/workflows/bidsReport.m @@ -22,7 +22,7 @@ function bidsReport(opt) opt.pipeline.type = 'preproc'; - if ~opt.boilerplate_only && ... + if ~opt.boilerplateOnly && ... isdir(fullfile(opt.dir.output, 'reports')) logger('INFO', 'Dataset reports already exist.', ... 'options', opt, ... diff --git a/src/workflows/preproc/bidsSmoothing.m b/src/workflows/preproc/bidsSmoothing.m index 1a519920c..af7247ae4 100644 --- a/src/workflows/preproc/bidsSmoothing.m +++ b/src/workflows/preproc/bidsSmoothing.m @@ -29,6 +29,10 @@ function bidsSmoothing(opt) [BIDS, opt] = setUpWorkflow(opt, 'smoothing data'); + if isempty(opt.taskName) + opt.taskName = bids.query(BIDS, 'tasks'); + end + allRT = {}; unRenamedFiles = {}; diff --git a/src/workflows/stats/bidsModelSelection.m b/src/workflows/stats/bidsModelSelection.m index bb177e60b..c094f8096 100644 --- a/src/workflows/stats/bidsModelSelection.m +++ b/src/workflows/stats/bidsModelSelection.m @@ -141,7 +141,7 @@ workflowName = 'macs model selection'; - [~, opt] = setUpWorkflow(opt, workflowName); + [~, opt] = setUpWorkflow(opt, workflowName, opt.dir.raw, true); opt.orderBatches.MACS_model_space = 1; switch lower(action) @@ -190,7 +190,8 @@ if isempty(spmMatFile) msg = sprintf('no SPM.mat found in:\n%s\n\n', ffxDir); id = 'noSPMmat'; - logger('ERROR', msg, 'id', id, 'filename', mfilename()); + logger('WARNING', msg, 'id', id, 'filename', mfilename()); + continue end msg = struct('Subject', subLabel); diff --git a/tests/data/models/model-default_smdl.json b/tests/data/models/model-default_smdl.json index c45abbe37..23461ae8e 100644 --- a/tests/data/models/model-default_smdl.json +++ b/tests/data/models/model-default_smdl.json @@ -34,7 +34,7 @@ "X": [ "trial_type.VisMot", "trial_type.VisStat", - 1, + "1", "trans_?", "rot_?", "non_steady_state_outlier*", diff --git a/tests/data/models/model-nback_smdl.json b/tests/data/models/model-nback_smdl.json index 2e384986b..2150d3f99 100644 --- a/tests/data/models/model-nback_smdl.json +++ b/tests/data/models/model-nback_smdl.json @@ -59,7 +59,7 @@ }, { "Level": "Subject", - "Name": "run", + "Name": "subject", "GroupBy": [ "contrast", "subject" diff --git a/tests/tests_bids_model/test_bidsModel.m b/tests/tests_bids_model/test_bidsModel.m index f45dba072..a628fe37a 100644 --- a/tests/tests_bids_model/test_bidsModel.m +++ b/tests/tests_bids_model/test_bidsModel.m @@ -163,7 +163,7 @@ function test_getVariablesToConvolve_warning() bm = BidsModel('file', opt.model.file); if bids.internal.is_octave() - return + moxunit_throw_test_skipped_exception('Octave:mixed-string-concat warning thrown'); end assertWarning(@()bm.getVariablesToConvolve('Name', 'dataset_level'), ... 'BidsModel:noVariablesToConvolve'); @@ -199,7 +199,7 @@ function test_getModelMask_method() assertEqual(mask, 'mask.nii'); if bids.internal.is_octave() - return + moxunit_throw_test_skipped_exception('Octave:mixed-string-concat warning thrown'); end bm.verbose = true; bm.Nodes{1}.Model.Options = rmfield(bm.Nodes{1}.Model.Options, 'Mask'); @@ -242,7 +242,7 @@ function test_getInclusiveMaskThreshold_method() bm.getInclusiveMaskThreshold('Name', 'subject_level'); if bids.internal.is_octave() - return + moxunit_throw_test_skipped_exception('Octave:mixed-string-concat warning thrown'); end assertWarning(@()bm.getInclusiveMaskThreshold('Name', 'subject_level'), ... 'BidsModel:noInclMaskThresh'); diff --git a/tests/tests_bids_model/test_getOptionsFromModel.m b/tests/tests_bids_model/test_getOptionsFromModel.m index dc17b1216..fa71291e0 100644 --- a/tests/tests_bids_model/test_getOptionsFromModel.m +++ b/tests/tests_bids_model/test_getOptionsFromModel.m @@ -11,10 +11,12 @@ function test_getOptionsFromModel_preproc() opt.pipeline.type = 'preproc'; + opt.pipeline.isBms = false; opt = getOptionsFromModel(opt); expectedOptions.pipeline.type = 'preproc'; + expectedOptions.pipeline.isBms = false; assertEqual(opt, expectedOptions); @@ -26,6 +28,7 @@ function test_getOptionsFromModel_no_model() opt.model.file = ''; opt.tolerant = true; opt.verbosity = 3; + opt.pipeline.isBms = false; assertExceptionThrown(@() getOptionsFromModel(opt), 'getOptionsFromModel:modelFileMissing'); @@ -37,10 +40,12 @@ function test_getOptionsFromModel_basic() opt.model.file = modelFile('dummy'); opt.verbosity = 0; opt.tolerant = true; + opt.pipeline.isBms = false; opt = getOptionsFromModel(opt); expectedOptions.pipeline.type = 'stats'; + expectedOptions.pipeline.isBms = false; expectedOptions.model.file = modelFile('dummy'); expectedOptions.verbosity = 0; expectedOptions.tolerant = true; @@ -66,6 +71,7 @@ function test_getOptionsFromModel_task() opt.model.file = modelFile('dummy'); opt.verbosity = 0; opt.tolerant = true; + opt.pipeline.isBms = false; opt = getOptionsFromModel(opt); @@ -90,6 +96,7 @@ function test_getOptionsFromModel_subject() opt.model.bm.Input.subject = {'02', '04'}; opt.verbosity = 0; opt.tolerant = true; + opt.pipeline.isBms = false; opt = getOptionsFromModel(opt); @@ -111,6 +118,7 @@ function test_getOptionsFromModel_space() opt.model.file = modelFile('vislocalizer'); opt.verbosity = 0; opt.tolerant = true; + opt.pipeline.isBms = false; opt = getOptionsFromModel(opt); @@ -131,6 +139,7 @@ function test_getOptionsFromModel_query() opt.query.acq = 'foo'; opt.verbosity = 0; opt.tolerant = true; + opt.pipeline.isBms = false; opt = getOptionsFromModel(opt); diff --git a/tests/tests_cli/test_bidspm_boilderplate.m b/tests/tests_cli/test_bidspm_boilderplate.m index 977a0c8d8..f17f1aa10 100644 --- a/tests/tests_cli/test_bidspm_boilderplate.m +++ b/tests/tests_cli/test_bidspm_boilderplate.m @@ -78,7 +78,7 @@ function test_boilerplate_only_preproc() bidspm(opt.dir.raw, outputPath, 'subject', ... 'action', 'preprocess', ... 'boilerplate_only', true, ... - 'task', {'auditory'}, ... + 'task', 'auditory', ... 'verbosity', 0, ... 'fwhm', 6); diff --git a/tests/tests_cli/test_bidspm_default_stats_model.m b/tests/tests_cli/test_bidspm_default_stats_model.m index 3f712b0af..2eabb52e3 100644 --- a/tests/tests_cli/test_bidspm_default_stats_model.m +++ b/tests/tests_cli/test_bidspm_default_stats_model.m @@ -63,7 +63,14 @@ function test_createDefaultStatsModel_CLI() expectedContent.Input.space = {'individual'}; if ~bids.internal.is_github_ci() - assertEqual(content.Nodes, expectedContent.Nodes); + for i = 1:numel(content.Nodes) + tmp = fieldnames(content.Nodes{i}); + for j = 1:numel(tmp) + assertEqual(content.Nodes{i}.(tmp{j}), ... + expectedContent.Nodes{i}.(tmp{j})); + end + end + assertEqual(content.Edges, expectedContent.Edges); assertEqual(content.Input, expectedContent.Input); end diff --git a/tests/tests_defaults/test_checkOptions.m b/tests/tests_defaults/test_checkOptions.m index 321be58b5..f6552012f 100644 --- a/tests/tests_defaults/test_checkOptions.m +++ b/tests/tests_defaults/test_checkOptions.m @@ -15,7 +15,10 @@ function test_checkOptions_basic() expectedOptions = defaultOptions('testTask'); - assertEqual(opt, expectedOptions); + toCheck = fieldnames(opt); + for i = 1:numel(toCheck) + assertEqual(opt.(toCheck{i}), expectedOptions.(toCheck{i})); + end end @@ -54,7 +57,7 @@ function test_checkOptions_do_not_overwrite() function test_checkOptions_error_task() if bids.internal.is_octave() - return + moxunit_throw_test_skipped_exception('Octave:mixed-string-concat warning thrown'); end opt.taskName = ''; diff --git a/tests/tests_reports/test_boilerplate.m b/tests/tests_reports/test_boilerplate.m index 921ba5a72..bb7a7523a 100644 --- a/tests/tests_reports/test_boilerplate.m +++ b/tests/tests_reports/test_boilerplate.m @@ -21,10 +21,10 @@ function test_boilerplate_spatial_preproc() for iUseUnwarp = 1:2 for iSpace = 1:numel(space) for fwhm = [0, 6] - for dummy_scans = [0, 4] + for dummyScans = [0, 4] printTestParameters(mfilename(), ... - dummy_scans, ... + dummyScans, ... space{iSpace}, ... useUnwarp{iUseUnwarp}, ... fwhm); @@ -35,7 +35,7 @@ function test_boilerplate_spatial_preproc() opt.stc.referenceSlice = 32; - opt.dummy_scans = dummy_scans; + opt.dummyScans = dummyScans; opt.space = space{iSpace}; opt.realign.useUnwarp = useUnwarp{iUseUnwarp}; opt.fwhm.func = fwhm; diff --git a/tests/tests_stats/subject_level/test_getEventSpecificationRoiGlm.m b/tests/tests_stats/subject_level/test_getEventSpecificationRoiGlm.m index c1d22711c..b72fd7a39 100644 --- a/tests/tests_stats/subject_level/test_getEventSpecificationRoiGlm.m +++ b/tests/tests_stats/subject_level/test_getEventSpecificationRoiGlm.m @@ -53,7 +53,7 @@ function test_getEventSpecificationRoiGlm_basic() function test_getEventSpecificationRoiGlm_warning_complex_contrasts() if bids.internal.is_octave() - return + moxunit_throw_test_skipped_exception('Octave:mixed-string-concat warning thrown'); end % GIVEN diff --git a/tests/tests_stats/subject_level/test_specifyContrasts.m b/tests/tests_stats/subject_level/test_specifyContrasts.m index fe7c5d7b4..62f6fc42e 100644 --- a/tests/tests_stats/subject_level/test_specifyContrasts.m +++ b/tests/tests_stats/subject_level/test_specifyContrasts.m @@ -300,9 +300,9 @@ function test_specifyContrasts_missing_condition_for_dummy_contrasts() SPM.xX.X = ones(1, numel(SPM.xX.name)); if bids.internal.is_octave() - % warning 'getRegressorIdx:missingRegressor' was raised, - % expected 'specifyContrasts:noContrast' - return + moxunit_throw_test_skipped_exception(['getRegressorIdx:missingRegressor ', ... + 'was raised expected ', ... + 'specifyContrasts:noContrast']); end assertWarning(@()specifyContrasts(model, SPM), ... @@ -336,9 +336,9 @@ function test_specifyContrasts_missing_condition() SPM.xX.X = ones(1, numel(SPM.xX.name)); if bids.internal.is_octave() - % warning 'getRegressorIdx:missingRegressor' was raised, - % expected 'specifyContrasts:noContrast' - return + moxunit_throw_test_skipped_exception(['getRegressorIdx:missingRegressor ', ... + 'was raised expected ', ... + 'specifyContrasts:noContrast']); end assertWarning(@()specifyContrasts(model, SPM), ... diff --git a/tests/tests_stats/unit_tests/test_getInclusiveMask.m b/tests/tests_stats/unit_tests/test_getInclusiveMask.m index 932f6dc65..f8328ceb2 100644 --- a/tests/tests_stats/unit_tests/test_getInclusiveMask.m +++ b/tests/tests_stats/unit_tests/test_getInclusiveMask.m @@ -58,7 +58,7 @@ function test_getInclusiveMask_basic() function test_getInclusiveMask_no_image() if bids.internal.is_octave() - return + moxunit_throw_test_skipped_exception('Octave:mixed-string-concat warning thrown'); end subLabel = '01'; diff --git a/tests/tests_stats/utils/test_getRegressorIdx.m b/tests/tests_stats/utils/test_getRegressorIdx.m index 47f97e07e..5c2f33f66 100644 --- a/tests/tests_stats/utils/test_getRegressorIdx.m +++ b/tests/tests_stats/utils/test_getRegressorIdx.m @@ -55,7 +55,7 @@ function test_getRegressorIdx_basic() %% missing if bids.internal.is_octave() - return + moxunit_throw_test_skipped_exception('Octave:mixed-string-concat warning thrown'); end assertWarning(@()getRegressorIdx('foo', SPM), 'getRegressorIdx:missingRegressor'); diff --git a/tests/tests_unit/test_copyGraphWindownOutput.m b/tests/tests_unit/test_copyGraphWindownOutput.m index de9204a2d..bfc69fe4e 100644 --- a/tests/tests_unit/test_copyGraphWindownOutput.m +++ b/tests/tests_unit/test_copyGraphWindownOutput.m @@ -36,7 +36,7 @@ function test_copyGraphWindownOutput_basic() function test_copyGraphWindownOutput_warning() if bids.internal.is_octave() - return + moxunit_throw_test_skipped_exception('Octave:mixed-string-concat warning thrown'); end [opt, subLabel, action] = setUp(); diff --git a/tests/tests_unit/test_displayArguments.m b/tests/tests_unit/test_displayArguments.m new file mode 100644 index 000000000..cdb232dbc --- /dev/null +++ b/tests/tests_unit/test_displayArguments.m @@ -0,0 +1,13 @@ +function test_suite = test_displayArguments %#ok<*STOUT> + % (C) Copyright 2023 bidspm developers + try % assignment of 'localfunctions' is necessary in Matlab >= 2016 + test_functions = localfunctions(); %#ok<*NASGU> + catch % no problem; early Matlab versions can use initTestSuite fine + end + initTestSuite; +end + +function test_displayArguments_basic() + + displayArguments(1, 'a', true, false, {'a', 'b', 'c'}, struct(), pwd); +end diff --git a/tests/tests_unit/test_removeDummies.m b/tests/tests_unit/test_removeDummies.m index ce47c7b90..c41f38cbf 100644 --- a/tests/tests_unit/test_removeDummies.m +++ b/tests/tests_unit/test_removeDummies.m @@ -33,7 +33,7 @@ function test_removeDummies_not_force() metadata.NumberOfVolumesDiscardedByUser = 10; if bids.internal.is_octave() - return + moxunit_throw_test_skipped_exception('Octave:mixed-string-concat warning thrown'); end assertWarning(@()removeDummies(inputFile, 4, metadata), ... 'removeDummies:dummiesAlreadyRemoved'); diff --git a/tests/tests_unit/test_utils.m b/tests/tests_unit/test_utils.m index 72e1398ee..76441bab7 100644 --- a/tests/tests_unit/test_utils.m +++ b/tests/tests_unit/test_utils.m @@ -1,6 +1,5 @@ -% (C) Copyright 2020 bidspm developers - function test_suite = test_utils %#ok<*STOUT> + % (C) Copyright 2020 bidspm developers try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> catch % no problem; early Matlab versions can use initTestSuite fine diff --git a/tests/tests_unit/test_warnings.m b/tests/tests_unit/test_warnings.m index cfdb9fc85..fec5d9a73 100644 --- a/tests/tests_unit/test_warnings.m +++ b/tests/tests_unit/test_warnings.m @@ -32,7 +32,7 @@ function test_noSPMmat() assertEqual(status, true); if bids.internal.is_octave() - return + moxunit_throw_test_skipped_exception('Octave:mixed-string-concat warning thrown'); end opt.verbosity = 1; assertWarning(@()noSPMmat(opt, subLabel, spmMatFile), 'noSPMmat:noSpecifiedModel'); @@ -50,7 +50,7 @@ function test_noRoiFound() assertEqual(status, true); if bids.internal.is_octave() - return + moxunit_throw_test_skipped_exception('Octave:mixed-string-concat warning thrown'); end opt.verbosity = 1; assertWarning(@()noRoiFound(opt, roiList), 'noRoiFound:noRoiFile'); @@ -60,7 +60,7 @@ function test_noRoiFound() function test_notImplemented() if bids.internal.is_octave() - return + moxunit_throw_test_skipped_exception('Octave:mixed-string-concat warning thrown'); end opt.verbosity = 1; assertWarning(@()notImplemented('foo', '', opt), 'foo:notImplemented'); @@ -84,7 +84,7 @@ function test_isTtest() assertEqual(status, false); if bids.internal.is_octave() - return + moxunit_throw_test_skipped_exception('Octave:mixed-string-concat warning thrown'); end assertWarning(@()isTtest(tmp), 'isTtest:notImplemented'); diff --git a/tests/utils/defaultOptions.m b/tests/utils/defaultOptions.m index ca2f7fe1b..46826f81e 100644 --- a/tests/utils/defaultOptions.m +++ b/tests/utils/defaultOptions.m @@ -22,20 +22,21 @@ 'to', 'T1w')); expectedOptions.pipeline.type = ''; + expectedOptions.pipeline.isBms = false; expectedOptions.pipeline.name = 'bidspm'; - expectedOptions.boilerplate_only = false; + expectedOptions.boilerplateOnly = false; expectedOptions.anatOnly = false; - expectedOptions.space = {'individual' 'IXI549Space'}; + expectedOptions.space = {'individual', 'IXI549Space'}; expectedOptions.useBidsSchema = false; expectedOptions.fwhm.func = 6; expectedOptions.fwhm.contrast = 0; - expectedOptions.dummy_scans = 0; + expectedOptions.dummyScans = 0; expectedOptions.stc.referenceSlice = []; expectedOptions.stc.skip = false;