From b799936224b3814681b1ec919407f8e18d3a6ac5 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 6 Mar 2021 18:20:30 +0100 Subject: [PATCH 001/145] add possibility to skip subject GLM QA --- demos/MoAE/MoAEpilot_run.m | 4 +--- initCppSpm.m | 12 +++++++++++ .../plot_power_spectra_of_GLM_residuals.m | 6 +++--- src/batches/setBatchEstimateModel.m | 21 +++++++++++++------ src/defaults/checkOptions.m | 9 ++++++++ src/workflows/bidsFFX.m | 16 +++++++------- tests/test_checkOptions.m | 2 ++ tests/test_loadAndCheckOptions.m | 2 ++ 8 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 initCppSpm.m diff --git a/demos/MoAE/MoAEpilot_run.m b/demos/MoAE/MoAEpilot_run.m index 0d56f37b..5ab325ff 100644 --- a/demos/MoAE/MoAEpilot_run.m +++ b/demos/MoAE/MoAEpilot_run.m @@ -18,9 +18,7 @@ % directory with this script becomes the current directory WD = fileparts(mfilename('fullpath')); -% we add all the subfunctions that are in the sub directories -addpath(genpath(fullfile(WD, '..', '..', 'src'))); -addpath(genpath(fullfile(WD, '..', '..', 'lib'))); +initCppSpm(); %% Set options opt = MoAEpilot_getOption(); diff --git a/initCppSpm.m b/initCppSpm.m new file mode 100644 index 00000000..788688e8 --- /dev/null +++ b/initCppSpm.m @@ -0,0 +1,12 @@ +% (C) Copyright 2021 CPP BIDS SPM-pipeline developers + +function initCppSpm() + + % directory with this script becomes the current directory + WD = fileparts(mfilename('fullpath')); + + % we add all the subfunctions that are in the sub directories + addpath(genpath(fullfile(WD, 'src'))); + addpath(genpath(fullfile(WD, 'lib'))); + +end diff --git a/lib/utils/plot_power_spectra_of_GLM_residuals.m b/lib/utils/plot_power_spectra_of_GLM_residuals.m index e4b55182..cb4b1b90 100644 --- a/lib/utils/plot_power_spectra_of_GLM_residuals.m +++ b/lib/utils/plot_power_spectra_of_GLM_residuals.m @@ -8,16 +8,16 @@ function plot_power_spectra_of_GLM_residuals(path_to_results, TR, cutoff_freq, a % -May 2018 % % -Given fMRI task results in AFNI, FSL or SPM, - % this script plots power spectra of GLM residuals. + % this script plots power spectra of GLM residuals. % -If there is strong structure visible in the GLM residuals - % the power spectra are not flat), the first level results are likely confounded. + % the power spectra are not flat), the first level results are likely confounded. % -tested on Linux % -you need on your path >= MATLAB 2017b, AFNI and FSL % -specify the default values for the cutoff frequency used by the high-pass filter, % -for the assumed experimental design frequency - % and for the true experimental design frequency; + % and for the true experimental design frequency; % -10 chosen, as it is beyond the plotted frequencies if ~exist('cutoff_freq', 'var') diff --git a/src/batches/setBatchEstimateModel.m b/src/batches/setBatchEstimateModel.m index 73601a25..fc6614b1 100644 --- a/src/batches/setBatchEstimateModel.m +++ b/src/batches/setBatchEstimateModel.m @@ -1,6 +1,6 @@ % (C) Copyright 2019 CPP BIDS SPM-pipeline developers -function matlabbatch = setBatchEstimateModel(matlabbatch, grpLvlCon, opt) +function matlabbatch = setBatchEstimateModel(matlabbatch, opt, grpLvlCon) % % Short description of what the function does goes here. % @@ -18,7 +18,7 @@ switch nargin - case 1 + case 2 printBatchName('estimate subject level fmri model'); @@ -30,7 +30,7 @@ '.', 'val', '{}', {1}), ... substruct('.', 'spmmat')); - matlabbatch = returnEstimateModelBatch(matlabbatch, spmMatFile); + matlabbatch = returnEstimateModelBatch(matlabbatch, spmMatFile, opt); case 3 @@ -42,7 +42,11 @@ spmMatFile = { fullfile(opt.rfxDir, conName, 'SPM.mat') }; - matlabbatch = returnEstimateModelBatch(matlabbatch, spmMatFile); + % no QA at the group level GLM: + % since there is no autocorrelation to check for + opt.glmQA.do = false(); + + matlabbatch = returnEstimateModelBatch(matlabbatch, spmMatFile, opt); end @@ -50,10 +54,15 @@ end -function matlabbatch = returnEstimateModelBatch(matlabbatch, spmMatFile) +function matlabbatch = returnEstimateModelBatch(matlabbatch, spmMatFile, opt) matlabbatch{end + 1}.spm.stats.fmri_est.method.Classical = 1; - matlabbatch{end}.spm.stats.fmri_est.write_residuals = 1; matlabbatch{end}.spm.stats.fmri_est.spmmat = spmMatFile; + writeResiduals = true(); + if ~opt.glmQA.do + writeResiduals = false(); + end + matlabbatch{end}.spm.stats.fmri_est.write_residuals = writeResiduals; + end diff --git a/src/defaults/checkOptions.m b/src/defaults/checkOptions.m index 12f060e9..641c65d5 100644 --- a/src/defaults/checkOptions.m +++ b/src/defaults/checkOptions.m @@ -51,6 +51,13 @@ % - ``opt.sliceOrder = []`` - To be used if SPM can't extract slice info. NOT RECOMMENDED: % if you know the order in which slices were acquired, you should be able to recompute % slice timing and add it to the json files in your BIDS data set. + % - ``opt.glmQA.do = true;`` - If set to ``true```the residual images of a + % GLM at the subject levels will be used to estimate if there is any remaining structure + % in the GLM residuals (the power spectra are not flat) that could indicate + % the subject level results are likely confounded (see + % ``plot_power_spectra_of_GLM_residuals``) and 'Accurate autocorrelation modeling + % substantially improves fMRI reliability' + % _https://www.nature.com/articles/s41467-019-09230-w.pdf % fieldsToSet = setDefaultOption(); @@ -101,6 +108,8 @@ fieldsToSet.model.hrfDerivatives = [0 0]; fieldsToSet.contrastList = {}; + fieldsToSet.glmQA.do = true; + % specify the results to compute fieldsToSet.result.Steps = returnDefaultResultsStructure(); diff --git a/src/workflows/bidsFFX.m b/src/workflows/bidsFFX.m index 843b0bf0..3ad26a53 100644 --- a/src/workflows/bidsFFX.m +++ b/src/workflows/bidsFFX.m @@ -60,7 +60,7 @@ function bidsFFX(action, opt, funcFWHM) 'sub-', subID, ... '_task-', opt.taskName, ... '_design_before_estimation']); - matlabbatch = setBatchEstimateModel(matlabbatch); + matlabbatch = setBatchEstimateModel(matlabbatch, opt); matlabbatch = setBatchPrintFigure(matlabbatch, [ ... 'sub-', subID, ... '_task-', opt.taskName, ... @@ -73,14 +73,16 @@ function bidsFFX(action, opt, funcFWHM) saveAndRunWorkflow(matlabbatch, batchName, opt, subID); - plot_power_spectra_of_GLM_residuals( ... - getFFXdir(subID, funcFWHM, opt), ... - opt.metadata.RepetitionTime); + if opt.glmQA.do + plot_power_spectra_of_GLM_residuals( ... + getFFXdir(subID, funcFWHM, opt), ... + opt.metadata.RepetitionTime); - deleteResidualImages(getFFXdir(subID, funcFWHM, opt)); + deleteResidualImages(getFFXdir(subID, funcFWHM, opt)); - movefile(['sub-', subID, '_task-', opt.taskName, '_design_*'], ... - getFFXdir(subID, funcFWHM, opt)); + movefile(['sub-', subID, '_task-', opt.taskName, '_design_*'], ... + getFFXdir(subID, funcFWHM, opt)); + end case 'contrasts' diff --git a/tests/test_checkOptions.m b/tests/test_checkOptions.m index eab892fe..158e4d40 100644 --- a/tests/test_checkOptions.m +++ b/tests/test_checkOptions.m @@ -105,6 +105,8 @@ function test_checkOptionsErrorVoxDim() expectedOptions.model.file = ''; expectedOptions.model.hrfDerivatives = [0 0]; + expectedOptions.glmQA.do = true; + expectedOptions.result.Steps = returnDefaultResultsStructure(); expectedOptions.parallelize.do = false; diff --git a/tests/test_loadAndCheckOptions.m b/tests/test_loadAndCheckOptions.m index 7da32e0a..dd35d983 100644 --- a/tests/test_loadAndCheckOptions.m +++ b/tests/test_loadAndCheckOptions.m @@ -137,6 +137,8 @@ function test_loadAndCheckOptionsFromSeveralFiles() expectedOptions.model.file = ''; expectedOptions.model.hrfDerivatives = [0 0]; + expectedOptions.glmQA.do = true; + expectedOptions.result.Steps = returnDefaultResultsStructure(); expectedOptions.parallelize.do = false; From a7be09428ac8cc47d3b2aafff5722fd2b049c3f2 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 6 Mar 2021 18:48:27 +0100 Subject: [PATCH 002/145] options are now saved in a cfg folder in the working directory --- .gitignore | 1 + .../MoAE/{ => cfg}/options_task-auditory.json | 0 ...ptions_task-auditory_space-individual.json | 0 .../options_task-auditory_unwarp-0.json | 0 ...sk-auditory_unwarp-0_space-individual.json | 0 .../options_task-balloonanalogrisktask.json | 0 ...alloonanalogrisktask_space-individual.json | 0 ...s_task-balloonanalogrisktask_unwarp-0.json | 0 ...logrisktask_unwarp-0_space-individual.json | 0 .../options_task-linebisection.json | 0 ...s_task-linebisection_space-individual.json | 0 .../options_task-linebisection_unwarp-0.json | 0 ...nebisection_unwarp-0_space-individual.json | 0 src/utils/loadAndCheckOptions.m | 4 ++- src/utils/saveOptions.m | 4 ++- tests/test_loadAndCheckOptions.m | 36 +++++++++++-------- 16 files changed, 28 insertions(+), 17 deletions(-) rename demos/MoAE/{ => cfg}/options_task-auditory.json (100%) rename demos/MoAE/{ => cfg}/options_task-auditory_space-individual.json (100%) rename demos/MoAE/{ => cfg}/options_task-auditory_unwarp-0.json (100%) rename demos/MoAE/{ => cfg}/options_task-auditory_unwarp-0_space-individual.json (100%) rename demos/openneuro/{options => cfg}/options_task-balloonanalogrisktask.json (100%) rename demos/openneuro/{options => cfg}/options_task-balloonanalogrisktask_space-individual.json (100%) rename demos/openneuro/{options => cfg}/options_task-balloonanalogrisktask_unwarp-0.json (100%) rename demos/openneuro/{options => cfg}/options_task-balloonanalogrisktask_unwarp-0_space-individual.json (100%) rename demos/openneuro/{options => cfg}/options_task-linebisection.json (100%) rename demos/openneuro/{options => cfg}/options_task-linebisection_space-individual.json (100%) rename demos/openneuro/{options => cfg}/options_task-linebisection_unwarp-0.json (100%) rename demos/openneuro/{options => cfg}/options_task-linebisection_unwarp-0_space-individual.json (100%) diff --git a/.gitignore b/.gitignore index 910d5fa3..0dd9ae12 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ demos/spm*/raw demos/spm*/source # test folder and dummy data +tests/cfg/*.json tests/sub-01/* tests/group/* tests/models/*.json diff --git a/demos/MoAE/options_task-auditory.json b/demos/MoAE/cfg/options_task-auditory.json similarity index 100% rename from demos/MoAE/options_task-auditory.json rename to demos/MoAE/cfg/options_task-auditory.json diff --git a/demos/MoAE/options_task-auditory_space-individual.json b/demos/MoAE/cfg/options_task-auditory_space-individual.json similarity index 100% rename from demos/MoAE/options_task-auditory_space-individual.json rename to demos/MoAE/cfg/options_task-auditory_space-individual.json diff --git a/demos/MoAE/options_task-auditory_unwarp-0.json b/demos/MoAE/cfg/options_task-auditory_unwarp-0.json similarity index 100% rename from demos/MoAE/options_task-auditory_unwarp-0.json rename to demos/MoAE/cfg/options_task-auditory_unwarp-0.json diff --git a/demos/MoAE/options_task-auditory_unwarp-0_space-individual.json b/demos/MoAE/cfg/options_task-auditory_unwarp-0_space-individual.json similarity index 100% rename from demos/MoAE/options_task-auditory_unwarp-0_space-individual.json rename to demos/MoAE/cfg/options_task-auditory_unwarp-0_space-individual.json diff --git a/demos/openneuro/options/options_task-balloonanalogrisktask.json b/demos/openneuro/cfg/options_task-balloonanalogrisktask.json similarity index 100% rename from demos/openneuro/options/options_task-balloonanalogrisktask.json rename to demos/openneuro/cfg/options_task-balloonanalogrisktask.json diff --git a/demos/openneuro/options/options_task-balloonanalogrisktask_space-individual.json b/demos/openneuro/cfg/options_task-balloonanalogrisktask_space-individual.json similarity index 100% rename from demos/openneuro/options/options_task-balloonanalogrisktask_space-individual.json rename to demos/openneuro/cfg/options_task-balloonanalogrisktask_space-individual.json diff --git a/demos/openneuro/options/options_task-balloonanalogrisktask_unwarp-0.json b/demos/openneuro/cfg/options_task-balloonanalogrisktask_unwarp-0.json similarity index 100% rename from demos/openneuro/options/options_task-balloonanalogrisktask_unwarp-0.json rename to demos/openneuro/cfg/options_task-balloonanalogrisktask_unwarp-0.json diff --git a/demos/openneuro/options/options_task-balloonanalogrisktask_unwarp-0_space-individual.json b/demos/openneuro/cfg/options_task-balloonanalogrisktask_unwarp-0_space-individual.json similarity index 100% rename from demos/openneuro/options/options_task-balloonanalogrisktask_unwarp-0_space-individual.json rename to demos/openneuro/cfg/options_task-balloonanalogrisktask_unwarp-0_space-individual.json diff --git a/demos/openneuro/options/options_task-linebisection.json b/demos/openneuro/cfg/options_task-linebisection.json similarity index 100% rename from demos/openneuro/options/options_task-linebisection.json rename to demos/openneuro/cfg/options_task-linebisection.json diff --git a/demos/openneuro/options/options_task-linebisection_space-individual.json b/demos/openneuro/cfg/options_task-linebisection_space-individual.json similarity index 100% rename from demos/openneuro/options/options_task-linebisection_space-individual.json rename to demos/openneuro/cfg/options_task-linebisection_space-individual.json diff --git a/demos/openneuro/options/options_task-linebisection_unwarp-0.json b/demos/openneuro/cfg/options_task-linebisection_unwarp-0.json similarity index 100% rename from demos/openneuro/options/options_task-linebisection_unwarp-0.json rename to demos/openneuro/cfg/options_task-linebisection_unwarp-0.json diff --git a/demos/openneuro/options/options_task-linebisection_unwarp-0_space-individual.json b/demos/openneuro/cfg/options_task-linebisection_unwarp-0_space-individual.json similarity index 100% rename from demos/openneuro/options/options_task-linebisection_unwarp-0_space-individual.json rename to demos/openneuro/cfg/options_task-linebisection_unwarp-0_space-individual.json diff --git a/src/utils/loadAndCheckOptions.m b/src/utils/loadAndCheckOptions.m index 1c10e0e0..72df76b5 100644 --- a/src/utils/loadAndCheckOptions.m +++ b/src/utils/loadAndCheckOptions.m @@ -25,7 +25,9 @@ % - add test for when the input is a structure. if nargin < 1 || isempty(optionJsonFile) - optionJsonFile = spm_select('FPList', pwd, '^options_task-.*.json$'); + optionJsonFile = spm_select('FPList', ... + fullfile(pwd, 'cfg'), ... + '^options_task-.*.json$'); end if isstruct(optionJsonFile) diff --git a/src/utils/saveOptions.m b/src/utils/saveOptions.m index 8f221439..aef5205b 100644 --- a/src/utils/saveOptions.m +++ b/src/utils/saveOptions.m @@ -12,7 +12,9 @@ function saveOptions(opt) % :type opt: structure % - filename = fullfile(pwd, ['options', ... + optionDir = fullfile(pwd, 'cfg'); + [~,~,~] = mkdir(optionDir); + filename = fullfile(optionDir, ['options', ... '_task-', opt.taskName, ... '_date-' datestr(now, 'yyyymmddHHMM'), ... '.json']); diff --git a/tests/test_loadAndCheckOptions.m b/tests/test_loadAndCheckOptions.m index 7da32e0a..e750bbdc 100644 --- a/tests/test_loadAndCheckOptions.m +++ b/tests/test_loadAndCheckOptions.m @@ -8,11 +8,12 @@ function test_loadAndCheckOptionsBasic() - delete('*.json'); + mkdir cfg; + delete(fullfile(pwd, 'cfg', '*.json')); % create dummy json file jsonContent.taskName = 'vismotion'; - filename = 'options_task-vismotion.json'; + filename = fullfile(pwd, 'cfg', 'options_task-vismotion.json'); spm_jsonwrite(filename, jsonContent); % makes sure that it is picked up by default @@ -27,6 +28,9 @@ function test_loadAndCheckOptionsBasic() function test_loadAndCheckOptionsStructure() + mkdir cfg; + delete(fullfile(pwd, 'cfg', '*.json')); + % create dummy json file opt.taskName = 'vismotion'; @@ -42,7 +46,8 @@ function test_loadAndCheckOptionsStructure() function test_loadAndCheckOptionsFromFile() - delete('*.json'); + mkdir cfg; + delete(fullfile(pwd, 'cfg', '*.json')); % create dummy json file jsonContent.taskName = 'vismotion'; @@ -50,11 +55,11 @@ function test_loadAndCheckOptionsFromFile() jsonContent.groups = {''}; jsonContent.subjects = {[]}; - filename = 'options_task-vismotion_space-T1w.json'; + filename = fullfile(pwd, 'cfg', 'options_task-vismotion_space-T1w.json'); spm_jsonwrite(filename, jsonContent); % makes sure that it is read correctly from - opt = loadAndCheckOptions('options_task-vismotion_space-T1w.json'); + opt = loadAndCheckOptions(filename); expectedOptions = defaultOptions(); expectedOptions.taskName = 'vismotion'; @@ -68,30 +73,31 @@ function test_loadAndCheckOptionsFromFile() function test_loadAndCheckOptionsFromSeveralFiles() - delete('*.json'); + mkdir cfg; + delete(fullfile(pwd, 'cfg', '*.json')); % create old dummy json file jsonContent.taskName = 'vismotion'; - filename = fullfile(pwd, ['options', ... - '_task-', jsonContent.taskName, ... - '_date-151501011111', ... - '.json']); + filename = fullfile(pwd, 'cfg', ['options', ... + '_task-', jsonContent.taskName, ... + '_date-151501011111', ... + '.json']); spm_jsonwrite(filename, jsonContent); % create dummy json file with no date jsonContent.taskName = 'vismotion'; jsonContent.space = 'individual'; - filename = 'options_task-vismotion_space-T1w.json'; + filename = fullfile(pwd, 'cfg', 'options_task-vismotion_space-T1w.json'); spm_jsonwrite(filename, jsonContent); % most recent option file that should be read from jsonContent.taskName = 'vismotion'; jsonContent.space = 'individual'; jsonContent.funcVoxelDims = [1 1 1]; - filename = fullfile(pwd, ['options', ... - '_task-', jsonContent.taskName, ... - '_date-' datestr(now, 'yyyymmddHHMM'), ... - '.json']); + filename = fullfile(pwd, 'cfg', ['options', ... + '_task-', jsonContent.taskName, ... + '_date-' datestr(now, 'yyyymmddHHMM'), ... + '.json']); spm_jsonwrite(filename, jsonContent); % makes sure that the right json is read From 9e7219f37c5371b2c0d40b00561ec1c9165207d4 Mon Sep 17 00:00:00 2001 From: marcobarilari Date: Mon, 8 Mar 2021 15:36:12 +0100 Subject: [PATCH 003/145] remove specific sourceMRI code which is not in common between the two repos --- demos/sourceDataProcessing/batchSource.m | 24 --- demos/sourceDataProcessing/getOptionSource.m | 53 ------- src/batches/setBatchGZip.m | 31 ---- src/defaults/checkOptionsSource.m | 74 --------- src/utils/convert3Dto4D.m | 154 ------------------- src/workflows/bidsGZipRawFolder.m | 41 ----- tests/test_checkOptionsSource.m | 51 ------ tests/test_setBatchGZip.m | 22 --- 8 files changed, 450 deletions(-) delete mode 100644 demos/sourceDataProcessing/batchSource.m delete mode 100644 demos/sourceDataProcessing/getOptionSource.m delete mode 100644 src/batches/setBatchGZip.m delete mode 100644 src/defaults/checkOptionsSource.m delete mode 100644 src/utils/convert3Dto4D.m delete mode 100644 src/workflows/bidsGZipRawFolder.m delete mode 100644 tests/test_checkOptionsSource.m delete mode 100644 tests/test_setBatchGZip.m diff --git a/demos/sourceDataProcessing/batchSource.m b/demos/sourceDataProcessing/batchSource.m deleted file mode 100644 index 7334115f..00000000 --- a/demos/sourceDataProcessing/batchSource.m +++ /dev/null @@ -1,24 +0,0 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - -clear; -clc; - -% Directory with this script becomes the current directory -pth = fileparts(mfilename('fullpath')); - -% We add all the subfunctions that are in the sub directories -addpath(genpath(fullfile(pth, '..', '..', 'src'))); -addpath(genpath(fullfile(pth, '..', '..', 'lib'))); - -%% Run batches - -optSource = getOptionSource(); - -% Single volumes to 4D volumes conversion + remove n dummies -convert3Dto4D(optSource); - -% Deface anatomical volumes in a raw folder -% defaceAnat(optSource); COMING SOON - -% GZip the volumes in a raw folder -bidsGZipRawFolder(optSource, 0); diff --git a/demos/sourceDataProcessing/getOptionSource.m b/demos/sourceDataProcessing/getOptionSource.m deleted file mode 100644 index 27689884..00000000 --- a/demos/sourceDataProcessing/getOptionSource.m +++ /dev/null @@ -1,53 +0,0 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - -function optSource = getOptionSource() - % - % Returns a structure that contains the options chosen by the user to run the source processing - % batch workflow - % - % USAGE:: - % - % optSource = getOptionSource() - % - % :returns: - :optSource: (struct) - - if nargin < 1 - optSource = []; - end - - % Set the folder where sequences folders exist - optSource.sourceDir = '/Users/barilari/Desktop/DICOM_UCL_leuven/renamed/sub-pilot001/ses-002/MRI'; - - optSource.dataDir = '/Users/barilari/Desktop/DICOM_UCL_leuven/raw'; - - % List of the sequences that you want to skip (folder name pattern) - optSource.sequenceToIgnore = {'AAHead_Scout', ... - 'b1map', ... - 't1', ... - 'gre_field'}; - - % Number of volumes to discard ad dummies, (0 is default) - optSource.nbDummies = 5; - - % List of the sequences where you want to remove dummies (folder name pattern) - optSource.sequenceRmDummies = {'cmrr_mbep2d_p3_mb2_1.6iso_AABrain', ... - 'cmrr_mbep2d_p4_mb2_750um_AAbrain'}; - - % Set data format conversion (0 is default) - - % 0: SAME - % 2: UINT8 - unsigned char - % 4: INT16 - signed short - % 8: INT32 - signed int - % 16: FLOAT32 - single prec. float - % 64: FLOAT64 - double prec. float - - optSource.dataType = 0; - - % Boolean to enable gzip of the new 4D file (0 is default) - optSource.zip = 0; - - % Check the options provided - optSource = checkOptionsSource(optSource); - -end diff --git a/src/batches/setBatchGZip.m b/src/batches/setBatchGZip.m deleted file mode 100644 index 81fb9fa5..00000000 --- a/src/batches/setBatchGZip.m +++ /dev/null @@ -1,31 +0,0 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - -function matlabbatch = setBatchGZip(matlabbatch, unzippedNiifiles, keepUnzippedNii) - % - % Set the batch for GZip the 4D volumes - % - % USAGE:: - % - % matlabbatch = setBatchGZip(matlabbatch, unzippedNiifiles, keepUnzippedNii = false) - % - % :param matlabbatch: - % :type matlabbatch: structure - % :param unzippedNiifiles: List of volumes to be gzipped - % :type unzippedNiifiles: array - % :param keepUnzippedNii: Boolean to decide to delete the unzipped files - % :type keepUnzippedNii: boolean - % - % :returns: - :matlabbatch: (struct) The matlabbath ready to run the spm job - - if nargin < 3 || isempty(keepUnzippedNii) - % delete the original unzipped .nii - keepUnzippedNii = false; - end - - printBatchName('zipping'); - - matlabbatch{end + 1}.cfg_basicio.file_dir.file_ops.cfg_gzip_files.files = unzippedNiifiles; - matlabbatch{end}.cfg_basicio.file_dir.file_ops.cfg_gzip_files.outdir = {''}; - matlabbatch{end}.cfg_basicio.file_dir.file_ops.cfg_gzip_files.keep = keepUnzippedNii; - -end diff --git a/src/defaults/checkOptionsSource.m b/src/defaults/checkOptionsSource.m deleted file mode 100644 index 3320f740..00000000 --- a/src/defaults/checkOptionsSource.m +++ /dev/null @@ -1,74 +0,0 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - -function optSource = checkOptionsSource(optSource) - % - % Check the option inputs for source data and add any missing field with some defaults - % - % USAGE:: - % - % optSource = checkOptionsSource(optSource) - % - % :param optSource: Obligatory argument. The structure that contains the options set by the user - % to run the batch workflow for source processing - % - % :returns: - :optSource: (struc) The structure with any unset fields with the deaufalt values - % - % OPTIONS (with their defaults): - % - ``optSource.sourceDir = ''`` - The directory where the source data are located. - % - ``optSource.dataDir = ''`` - The directory where the raw data to apply changes are located. - % - ``optSource.sequenceToIgnore = {}`` - The list of sequence(s) to ignore. - % - ``optSource.dataType = 0`` - Data format conversion (0 is reccomended). - % - ``optSource.zip = 0`` - Boolean to enable gzip of the new 4D file in ``convert3Dto4D``. - % - ``optSource.nbDummies = 0`` - Number of volumes to discard ad dummies in ``convert3Dto4D``. - % - ``optSource.sequenceRmDummies = {}`` - The list of sequence(s) where to discarding the - % dummies. - - fieldsToSet = setDefaultOptionSource(); - - optSource = setDefaultFields(optSource, fieldsToSet); - - if isempty(optSource.sourceDir) || ~isdir(optSource.sourceDir) - - warning('The source folder is not provided or does not exist.'); - - end - - if isempty(optSource.dataDir) || ~isdir(optSource.dataDir) - - warning('The raw folder is not provided or does not exist.'); - - end - - if isempty(optSource.sequenceToIgnore) - - warning('No sequence-to-ignore provided, I will convert all the images that I can found'); - - end - -end - -function fieldsToSet = setDefaultOptionSource() - % This defines the missing fields - - % The directory where the source data are located - fieldsToSet.sourceDir = ''; - - % The directory where the raw data to apply changes are located - fieldsToSet.dataDir = ''; - - % The list of sequence(s) to ignore - fieldsToSet.sequenceToIgnore = {}; - - % Data format conversion (0 is reccomended) - fieldsToSet.dataType = 0; - - % Boolean to enable gzip of the new 4D file - fieldsToSet.zip = 0; - - % Number of volumes to discard ad dummies - fieldsToSet.nbDummies = 0; - - % The list of sequence(s) where to discarding the dummies - fieldsToSet.sequenceRmDummies = {}; - -end diff --git a/src/utils/convert3Dto4D.m b/src/utils/convert3Dto4D.m deleted file mode 100644 index bafea7e0..00000000 --- a/src/utils/convert3Dto4D.m +++ /dev/null @@ -1,154 +0,0 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - -function convert3Dto4D(optSource) - % - % It converts single volumes of a sequence in a 4D file, remove the dummies (optional), zip the - % 4D file (optional) and delete the converted files. Recursevly loops through a folder in which a - % not-yet-BIDS dataset live and the nii files are sorted in each sequence folder. - % - % USAGE:: - % - % convert3Dto4D(optSource) - % - % :param optSource: Obligatory argument. The structure that contains the options set by the user - % to run the batch workflow for source processing - % - % .. todo: - % - % - expand to run through multiple subjs ans groups - % (https://stackoverflow.com/questions/8748976/ - % list-the-subfolders-in-a-folder-matlab-only-subfolders-not-files) - % - generalize how to retrieve RT from sidecar json file - % - saveMatlabBatch(matlabbatch, ... - % ['3Dto4D_dataType-' num2str(dataType) '_RT-' num2str(RT)], opt, subID); - % - Cover the MoCo use case: if the sequence is MoCo (motion corrected when the "scanner" - % reconstructs the images - an option on can tick on Siemens scanner and that output an - % additional MoCo file with the regular sequence) then each JSON file of each volume contains - % the motion correction information for that volume. So only taking the JSON of the first - % volume means we "lose" the realignment parameters that could be useful later. - - % Get source folder content - sourceDataStruc = dir(optSource.sourceDir); - - isDir = [sourceDataStruc(:).isdir]; - - optSource.sequenceList = {sourceDataStruc(isDir).name}'; - - % Loop through the sequence folders - - tic; - - for iSeq = 1:size(optSource.sequenceList, 1) - - % Skip 'non' folders - if length(optSource.sequenceList{iSeq}) > 2 - - % Check if sequence to ignore or not - if contains(optSource.sequenceList(iSeq), optSource.sequenceToIgnore) - - warning('\nIGNORING SEQUENCE: %s\n', string(optSource.sequenceList(iSeq))); - - else - - fprintf('\n\nCONVERTING SEQUENCE: %s \n', char(optSource.sequenceList(iSeq))); - - % Set whether to remove dummies or not - - nbDummies = 0; - - if contains(optSource.sequenceList(iSeq), optSource.sequenceRmDummies) - - nbDummies = optSource.nbDummies; - - fprintf('\n\nREMOVING %s DUMMIES\n\n', num2str(nbDummies)); - - end - - % Get sequence folder path - sequencePath = fullfile(optSource.sourceDir, optSource.sequenceList{iSeq}); - - % Retrieve volume files info - [volumesList, outputNameImage] = parseFiles('nii', sequencePath, nbDummies); - - % Set output name, it takes the file name of the 1st volume of the 4D file and add subfix - outputNameImage = strrep(outputNameImage, '.nii', '_4D.nii'); - - % Retrieve sidecar json files info - [jsonList, outputNameJson] = parseFiles('json', sequencePath, nbDummies); - - jsonFile = spm_jsonread(jsonList{1}); - - % % % % % % LIEGE SPECIFIC % % % % % % % - RT = jsonFile.acqpar.RepetitionTime / 1000; - % % % % % % % % % % % % % % % % % % % % % - - % Set and run spm batch, input all the volumes minus the dummies if > 0 - matlabbatch = []; - matlabbatch = setBatch3Dto4D(matlabbatch, ... - volumesList(nbDummies + 1:end, :), ... - RT, ... - outputNameImage, ... - optSource.dataType); - - spm_jobman('run', matlabbatch); - - if optSource.zip - - % Zip and delete the and the new 4D file - fprintf(1, 'ZIP AND DELETE THE NEW 4D BRAIN \n\n'); - - gzip([sequencePath filesep outputNameImage]); - - delete([sequencePath filesep outputNameImage]); - - end - - % Save one sidecar json file, it takes the file name of the 1st volume of the 4D file and - % add subfix - if ~isempty(jsonList) - - copyfile(jsonList{1}, [sequencePath filesep strrep(outputNameJson, '.json', '_4D.json')]); - - end - - % Delete all the single volumes .nii and .json files - fprintf(1, 'EXTERMINATE SINGLE VOLUMES FILES \n\n'); - - for iDel = 1:length(volumesList) - - delete(volumesList{iDel}); - delete(jsonList{iDel}); - - end - - end - - end - - end - - toc; - -end - -function [fileList, outputName] = parseFiles(fileExtention, sequencePath, nbDummies) - - fileList = spm_select('list', sequencePath, fileExtention); - - if size(fileList, 1) > 0 - - outputName = fileList(nbDummies + 1, :); - - fileList = strcat(sequencePath, filesep, cellstr(fileList)); - - else - - fileList = {}; - - outputName = []; - - warning('\nI have found 0 files with extension ''.%s'' \n', fileExtention); - - end - -end diff --git a/src/workflows/bidsGZipRawFolder.m b/src/workflows/bidsGZipRawFolder.m deleted file mode 100644 index 99003501..00000000 --- a/src/workflows/bidsGZipRawFolder.m +++ /dev/null @@ -1,41 +0,0 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - -function bidsGZipRawFolder(opt, keepUnzippedNii) - % - % - % GZip the nii files in a ``raw`` bids folders from the. It will do it independently of the task. - % - % USAGE:: - % - % bidsGZipRawFolder(optSource ... - % [, keepUnzippedNii = false]) - % - % :param opt: The structure that contains the options set by the user to run the batch - % workflow for source processing - % :type opt: structure - % :param keepUnzippedNii: will keep the original ``.nii`` if set to ``true``. Default is false - % :type keepUnzippedNii: boolean - - %% input variables default values - - if nargin < 2 || isempty(keepUnzippedNii) - % delete the original unzipped .nii - keepUnzippedNii = false; - end - - tic; - - printWorklowName('GZip data'); - - rawDir = opt.dataDir; - - unzippedNiifiles = cellstr(spm_select('FPListRec', rawDir, '^.*.nii$')); - - matlabbatch = []; - matlabbatch = setBatchGZip(matlabbatch, unzippedNiifiles, keepUnzippedNii); - - spm_jobman('run', matlabbatch); - - toc; - -end diff --git a/tests/test_checkOptionsSource.m b/tests/test_checkOptionsSource.m deleted file mode 100644 index 1d0f69a9..00000000 --- a/tests/test_checkOptionsSource.m +++ /dev/null @@ -1,51 +0,0 @@ -function test_suite = test_checkOptionsSource %#ok<*STOUT> - 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_checkOptionsSourceBasic() - - optSource.nbDummies = 0; - optSource = checkOptionsSource(optSource); - - expectedOptionsSource = defaultOptionsSource(); - expectedOptionsSource.nbDummies = 0; - - assertEqual(optSource, expectedOptionsSource); - -end - -function test_checkOptionsSourceDoNotOverwrite() - - optSource.dataType = 666; - optSource.someExtraField = 'test'; - optSource.nbDummies = 42; - - optSource = checkOptionsSource(optSource); - - assertEqual(optSource.dataType, 666); - assertEqual(optSource.someExtraField, 'test'); - assertEqual(optSource.nbDummies, 42); - -end - -function expectedOptionsSource = defaultOptionsSource() - - expectedOptionsSource.sourceDir = ''; - - expectedOptionsSource.dataDir = ''; - - expectedOptionsSource.sequenceToIgnore = {}; - - expectedOptionsSource.dataType = 0; - - expectedOptionsSource.zip = 0; - - expectedOptionsSource.nbDummies = 0; - - expectedOptionsSource.sequenceRmDummies = {}; - -end diff --git a/tests/test_setBatchGZip.m b/tests/test_setBatchGZip.m deleted file mode 100644 index 2651e069..00000000 --- a/tests/test_setBatchGZip.m +++ /dev/null @@ -1,22 +0,0 @@ -function test_suite = test_setBatchGZip %#ok<*STOUT> - 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_setBatchGZipBasic() - - unzippedNiifiles = 'sub-01_ses-01_T1w.nii'; - - matlabbatch = []; - matlabbatch = setBatchGZip(matlabbatch, unzippedNiifiles); - - expectedBatch{1}.cfg_basicio.file_dir.file_ops.cfg_gzip_files.files = 'sub-01_ses-01_T1w.nii'; - expectedBatch{1}.cfg_basicio.file_dir.file_ops.cfg_gzip_files.outdir = {''}; - expectedBatch{1}.cfg_basicio.file_dir.file_ops.cfg_gzip_files.keep = false(); - - assertEqual(matlabbatch, expectedBatch); - -end From daee136c8bc8adf3083311e4c17f230e5bbd4bf2 Mon Sep 17 00:00:00 2001 From: Michele MacLean Date: Mon, 8 Mar 2021 11:57:03 -0500 Subject: [PATCH 004/145] add DetectLesion batch --- src/batches/setBatchDetectLesion.m | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100755 src/batches/setBatchDetectLesion.m diff --git a/src/batches/setBatchDetectLesion.m b/src/batches/setBatchDetectLesion.m new file mode 100755 index 00000000..2e796d56 --- /dev/null +++ b/src/batches/setBatchDetectLesion.m @@ -0,0 +1,23 @@ +%----------------------------------------------------------------------- +% Job saved on 08-Mar-2021 11:11:19 by cfg_util (rev $Rev: 7345 $) +% spm SPM - SPM12 (7771) +% cfg_basicio BasicIO - Unknown +%----------------------------------------------------------------------- +matlabbatch{1}.spm.tools.ali.unified_segmentation.step1data = ''; +matlabbatch{1}.spm.tools.ali.unified_segmentation.step1prior = {'C:\Users\michm\spm12\toolbox\ALI\Priors_extraClass\wc4prior0.nii'}; +matlabbatch{1}.spm.tools.ali.unified_segmentation.step1niti = 2; +matlabbatch{1}.spm.tools.ali.unified_segmentation.step1thr_prob = 0.333333333333333; +matlabbatch{1}.spm.tools.ali.unified_segmentation.step1thr_size = 0.8; +matlabbatch{1}.spm.tools.ali.unified_segmentation.step1coregister = 1; +matlabbatch{1}.spm.tools.ali.unified_segmentation.step1mask = {''}; +matlabbatch{1}.spm.tools.ali.unified_segmentation.step1vox = 2; +matlabbatch{1}.spm.tools.ali.unified_segmentation.step1fwhm = [8 8 8]; +matlabbatch{2}.spm.tools.ali.outliers_detection.step3tissue.step3patients = ''; +matlabbatch{2}.spm.tools.ali.outliers_detection.step3tissue.step3controls = ''; +matlabbatch{2}.spm.tools.ali.outliers_detection.step3tissue.step3Alpha = 0.5; +matlabbatch{2}.spm.tools.ali.outliers_detection.step3tissue.step3Lambda = -4; +matlabbatch{2}.spm.tools.ali.outliers_detection.step3mask = {'C:\Users\michm\spm12\toolbox\ALI\Mask_image\mask_controls_vox2mm.nii'}; +matlabbatch{2}.spm.tools.ali.outliers_detection.step3mask_thr = 0; +matlabbatch{2}.spm.tools.ali.outliers_detection.step3binary_thr = 0.3; +matlabbatch{2}.spm.tools.ali.outliers_detection.step3binary_size = 0.8; +matlabbatch{3}.spm.tools.ali.lesion_overlap.lom = ''; From bbd4e233e71349d4afd20866051cdd8f2b97beca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mich=C3=A8le=20MacLean?= <54547865+mwmaclean@users.noreply.github.com> Date: Mon, 8 Mar 2021 12:30:38 -0500 Subject: [PATCH 005/145] Apply suggestions from code review Change path Co-authored-by: Remi Gau --- src/batches/setBatchDetectLesion.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/batches/setBatchDetectLesion.m b/src/batches/setBatchDetectLesion.m index 2e796d56..ad516821 100755 --- a/src/batches/setBatchDetectLesion.m +++ b/src/batches/setBatchDetectLesion.m @@ -4,7 +4,9 @@ % cfg_basicio BasicIO - Unknown %----------------------------------------------------------------------- matlabbatch{1}.spm.tools.ali.unified_segmentation.step1data = ''; -matlabbatch{1}.spm.tools.ali.unified_segmentation.step1prior = {'C:\Users\michm\spm12\toolbox\ALI\Priors_extraClass\wc4prior0.nii'}; +spmDir = spm('dir'); +lesionPriorMap = fullfile(spmDir, 'toolbox', 'ALI', 'Priors_extraClass', 'wc4prior0.nii) +matlabbatch{1}.spm.tools.ali.unified_segmentation.step1prior = {lesionPriorMap}; matlabbatch{1}.spm.tools.ali.unified_segmentation.step1niti = 2; matlabbatch{1}.spm.tools.ali.unified_segmentation.step1thr_prob = 0.333333333333333; matlabbatch{1}.spm.tools.ali.unified_segmentation.step1thr_size = 0.8; From 2d3164220826371e4e1cb8b2cd76b8a70c0c4dab Mon Sep 17 00:00:00 2001 From: Michele MacLean Date: Mon, 8 Mar 2021 14:39:04 -0500 Subject: [PATCH 006/145] add 3 batches for lesion detection --- src/batches/setBatchAbnormalitiesDetectLesion.m | 14 ++++++++++++++ src/batches/setBatchLesionOverlapMap.m | 6 ++++++ ...Lesion.m => setBatchSegmentationDetectLesion.m} | 11 +---------- 3 files changed, 21 insertions(+), 10 deletions(-) create mode 100755 src/batches/setBatchAbnormalitiesDetectLesion.m create mode 100755 src/batches/setBatchLesionOverlapMap.m rename src/batches/{setBatchDetectLesion.m => setBatchSegmentationDetectLesion.m} (56%) diff --git a/src/batches/setBatchAbnormalitiesDetectLesion.m b/src/batches/setBatchAbnormalitiesDetectLesion.m new file mode 100755 index 00000000..337afe71 --- /dev/null +++ b/src/batches/setBatchAbnormalitiesDetectLesion.m @@ -0,0 +1,14 @@ +%----------------------------------------------------------------------- +% Job saved on 08-Mar-2021 12:47:16 by cfg_util (rev $Rev: 7345 $) +% spm SPM - SPM12 (7771) +% cfg_basicio BasicIO - Unknown +%----------------------------------------------------------------------- +matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3patients = ''; +matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3controls = ''; +matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3Alpha = 0.5; +matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3Lambda = -4; +spmDir = spm('dir'); +lesionMask = fullfile(spmDir, 'toolbox', 'ALI', 'Mask_image', 'mask_controls_vox2mm.nii'); +matlabbatch{1}.spm.tools.ali.outliers_detection.step3mask_thr = 0; +matlabbatch{1}.spm.tools.ali.outliers_detection.step3binary_thr = 0.3; +matlabbatch{1}.spm.tools.ali.outliers_detection.step3binary_size = 0.8; diff --git a/src/batches/setBatchLesionOverlapMap.m b/src/batches/setBatchLesionOverlapMap.m new file mode 100755 index 00000000..9cde37ad --- /dev/null +++ b/src/batches/setBatchLesionOverlapMap.m @@ -0,0 +1,6 @@ +%----------------------------------------------------------------------- +% Job saved on 08-Mar-2021 12:48:14 by cfg_util (rev $Rev: 7345 $) +% spm SPM - SPM12 (7771) +% cfg_basicio BasicIO - Unknown +%----------------------------------------------------------------------- +matlabbatch{1}.spm.tools.ali.lesion_overlap.lom = ''; diff --git a/src/batches/setBatchDetectLesion.m b/src/batches/setBatchSegmentationDetectLesion.m similarity index 56% rename from src/batches/setBatchDetectLesion.m rename to src/batches/setBatchSegmentationDetectLesion.m index ad516821..eca11150 100755 --- a/src/batches/setBatchDetectLesion.m +++ b/src/batches/setBatchSegmentationDetectLesion.m @@ -5,7 +5,7 @@ %----------------------------------------------------------------------- matlabbatch{1}.spm.tools.ali.unified_segmentation.step1data = ''; spmDir = spm('dir'); -lesionPriorMap = fullfile(spmDir, 'toolbox', 'ALI', 'Priors_extraClass', 'wc4prior0.nii) +lesionPriorMap = fullfile(spmDir, 'toolbox', 'ALI', 'Priors_extraClass', 'wc4prior0.nii'); matlabbatch{1}.spm.tools.ali.unified_segmentation.step1prior = {lesionPriorMap}; matlabbatch{1}.spm.tools.ali.unified_segmentation.step1niti = 2; matlabbatch{1}.spm.tools.ali.unified_segmentation.step1thr_prob = 0.333333333333333; @@ -14,12 +14,3 @@ matlabbatch{1}.spm.tools.ali.unified_segmentation.step1mask = {''}; matlabbatch{1}.spm.tools.ali.unified_segmentation.step1vox = 2; matlabbatch{1}.spm.tools.ali.unified_segmentation.step1fwhm = [8 8 8]; -matlabbatch{2}.spm.tools.ali.outliers_detection.step3tissue.step3patients = ''; -matlabbatch{2}.spm.tools.ali.outliers_detection.step3tissue.step3controls = ''; -matlabbatch{2}.spm.tools.ali.outliers_detection.step3tissue.step3Alpha = 0.5; -matlabbatch{2}.spm.tools.ali.outliers_detection.step3tissue.step3Lambda = -4; -matlabbatch{2}.spm.tools.ali.outliers_detection.step3mask = {'C:\Users\michm\spm12\toolbox\ALI\Mask_image\mask_controls_vox2mm.nii'}; -matlabbatch{2}.spm.tools.ali.outliers_detection.step3mask_thr = 0; -matlabbatch{2}.spm.tools.ali.outliers_detection.step3binary_thr = 0.3; -matlabbatch{2}.spm.tools.ali.outliers_detection.step3binary_size = 0.8; -matlabbatch{3}.spm.tools.ali.lesion_overlap.lom = ''; From 49e01ea3462330c0aa30ab4764e99397f1f88df0 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Tue, 9 Mar 2021 07:32:04 +0100 Subject: [PATCH 007/145] add function to return list of subject to analyze --- initCppSpm.m | 7 ++- src/utils/getSubjectList.m | 51 ++++++++++++++++++++++ tests/test_getSubjectList.m | 85 +++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 src/utils/getSubjectList.m create mode 100644 tests/test_getSubjectList.m diff --git a/initCppSpm.m b/initCppSpm.m index 788688e8..dbf73369 100644 --- a/initCppSpm.m +++ b/initCppSpm.m @@ -7,6 +7,11 @@ function initCppSpm() % we add all the subfunctions that are in the sub directories addpath(genpath(fullfile(WD, 'src'))); - addpath(genpath(fullfile(WD, 'lib'))); + addpath(genpath(fullfile(WD, 'lib', 'mancoreg'))); + addpath(genpath(fullfile(WD, 'lib', 'NiftiTools'))); + addpath(genpath(fullfile(WD, 'lib', 'spmup'))); + addpath(genpath(fullfile(WD, 'lib', 'utils'))); + + addpath(fullfile(WD, 'lib', 'bids-matlab')); end diff --git a/src/utils/getSubjectList.m b/src/utils/getSubjectList.m new file mode 100644 index 00000000..e97336fc --- /dev/null +++ b/src/utils/getSubjectList.m @@ -0,0 +1,51 @@ +% (C) Copyright 2021 CPP BIDS SPM-pipeline developers + +function opt = getSubjectList(BIDS, opt) + + allSubjects = bids.query(BIDS, 'subjects'); + + % Whatever subject entered must be returned "linearized" + tmp = opt.subjects; + tmp = tmp(:); + + % if any group is mentioned + if ~isempty(opt.groups{1}) && ... + any(strcmpi({'group'}, fieldnames(BIDS.participants))) + + fields = fieldnames(BIDS.participants); + fieldIdx = strcmpi({'group'}, fields); + + subjectIdx = strcmp(BIDS.participants.(fields{fieldIdx}), opt.groups); + + subjects = char(BIDS.participants.participant_id); + subjects = cellstr(subjects(subjectIdx, 5:end)); + + tmp = cat(1, tmp, subjects); + + end + + % If no subject specified so far we take all subjects + if isempty(tmp) || iscell(tmp) && isempty(tmp{1}) + tmp = allSubjects; + end + + % remove duplicates + opt.subjects = unique(tmp); + + if size(opt.subjects, 1) == 1 + opt.subjects = opt.subjects'; + end + + % check that all the subjects asked for exist + if any(~ismember(opt.subjects, allSubjects)) + fprintf('subjects specified\n'); + disp(opt.subjects); + fprintf('subjects present\n'); + disp(allSubjects); + + errorStruct.identifier = 'getSubjectList:noMatchingSubject'; + errorStruct.message = 'Some of the subjects specified do not exist in this data set.'; + error(errorStruct); + end + +end diff --git a/tests/test_getSubjectList.m b/tests/test_getSubjectList.m new file mode 100644 index 00000000..25e0e12c --- /dev/null +++ b/tests/test_getSubjectList.m @@ -0,0 +1,85 @@ +function test_suite = test_getSubjectList %#ok<*STOUT> + 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_getSubjectListNone() + + opt = setOptions(); + + opt = checkOptions(opt); + + BIDS = bids.layout(opt.derivativesDir); + + %% Get all groups all subjects + opt = getSubjectList(BIDS, opt); + + assertEqual(opt.subjects, ... + {'01' '02' 'blind01' 'blind02' 'ctrl01' 'ctrl02'}'); + +end + +function test_getSubjectListGroup() + + opt = setOptions(); + + opt = checkOptions(opt); + + BIDS = bids.layout(opt.derivativesDir); + + %% Get all subjects of a group and a subject from another group + opt.groups = {'blind'}; + opt.subjects = {'ctrl01'}; + + opt = getSubjectList(BIDS, opt); + + % 'sub-02' is defined a blind in the participants.tsv + assertEqual(opt.subjects, {'02', 'blind01', 'blind02', 'ctrl01'}'); + +end + +function test_getSubjectListBasic() + + opt = setOptions(); + + opt = checkOptions(opt); + + BIDS = bids.layout(opt.derivativesDir); + + %% Get some specified subjects + opt.groups = {''}; + opt.subjects = {'01', '02'; 'ctrl02', 'blind02'}; + + opt = getSubjectList(BIDS, opt); + + assertEqual(opt.subjects, {'01', '02', 'blind02', 'ctrl02'}'); + +end + +function test_getSubjectListErrorSubject() + + opt = setOptions(); + + opt.subjects = {'03'}; + + opt = checkOptions(opt); + + BIDS = bids.layout(opt.derivativesDir); + + assertExceptionThrown( ... + @()getSubjectList(BIDS, opt), ... + 'getSubjectList:noMatchingSubject'); + +end + +function opt = setOptions() + opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), ... + 'dummyData', ... + 'derivatives', ... + 'cpp_spm'); + opt.taskName = 'vismotion'; + opt.zeropad = 2; +end From 116a51f482d841a41937a8c3455e1885452b1aeb Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Tue, 9 Mar 2021 07:46:57 +0100 Subject: [PATCH 008/145] update getData and doc --- src/getData.m | 75 ++----------------- src/getSpecificSubjects.m | 61 --------------- src/utils/getSubjectList.m | 37 +++++++++ .../derivatives/cpp_spm/participants.tsv | 13 ++-- tests/test_getData.m | 65 ---------------- 5 files changed, 52 insertions(+), 199 deletions(-) delete mode 100644 src/getSpecificSubjects.m diff --git a/src/getData.m b/src/getData.m index d28da2b2..c0dca3d6 100644 --- a/src/getData.m +++ b/src/getData.m @@ -1,12 +1,12 @@ % (C) Copyright 2020 CPP BIDS SPM-pipeline developers -function [group, opt, BIDS] = getData(opt, BIDSdir, type) +function [BIDS, opt] = getData(opt, BIDSdir, type) % - % Short description of what the function does goes here. + % Reads the specified BIDS data set and updates the list of subjects to analyze. % % USAGE:: % - % [group, opt, BIDS] = getData(opt, [BIDSdir], [type = 'bold']) + % [BIDS, opt] = getData(opt, [BIDSdir], [type = 'bold']) % % :param opt: Options chosen for the analysis. See ``checkOptions()``. % :type opt: structure @@ -17,41 +17,10 @@ % supported: ``'bold'`` (default) and ``T1w`` % :type type: string % - % :returns: - :group: (structure) + % :returns: % - :opt: (structure) % - :BIDS: (structure) % - % ``getData()`` reads the specified BIDS data set and gets the groups and - % subjects to analyze. This can be specified in the opt structure in different ways. - % - % Set the group of subjects to analyze:: - % - % opt.groups = {'control', 'blind'}; - % - % If there are no groups (i.e subjects names are of the form ``sub-01`` for - % example) or if you want to run all subjects of all groups then use:: - % - % opt.groups = {''}; - % opt.subjects = {[]}; - % - % If you have 2 groups (``cont`` and ``cat`` for example) the following will - % run ``cont01``, ``cont02``, ``cat03``, ``cat04``:: - % - % opt.groups = {'cont', 'cat'}; - % opt.subjects = {[1 2], [3 4]}; - % - % If you have more than 2 groups but want to only run the subjects of 2 - % groups then you can use:: - % - % opt.groups = {'cont', 'cat'}; - % opt.subjects = {[], []}; - % - % You can also directly specify the subject label for the participants you - % want to run:: - % - % opt.groups = {''}; - % opt.subjects = {'01', 'cont01', 'cat02', 'ctrl02', 'blind01'}; - % % .. todo % Check if the following is true? Ideally write a test to make sure. % @@ -88,43 +57,15 @@ end % get IDs of all subjects - subjects = bids.query(BIDS, 'subjects'); + opt = getSubjectList(BIDS, opt); % get metadata for bold runs for that task % we take those from the first run of the first subject assuming it can % apply to all others. - opt = getMetaData(BIDS, opt, subjects, type); - - %% Add the different groups in the experiment - for iGroup = 1:numel(opt.groups) % for each group - - clear idx; - - % Name of the group - group(iGroup).name = opt.groups{iGroup}; %#ok<*AGROW> - - group = getSpecificSubjects(opt, group, iGroup, subjects); + opt = getMetaData(BIDS, opt, opt.subjects, type); - % check that all the subjects asked for exist - if ~all(ismember(group(iGroup).subNumber, subjects)) - fprintf('subjects specified\n'); - disp(group(iGroup).subNumber); - fprintf('subjects present\n'); - disp(subjects); - - errorStruct.identifier = 'getData:noMatchingSubject'; - msg = ['Some of the subjects specified do not exist in this data set.' ... - 'This can be due to wrong zero padding: see opt.zeropad in getOptions']; - errorStruct.message = msg; - error(errorStruct); - end - - % Number of subjects in the group - group(iGroup).numSub = length(group(iGroup).subNumber); - - fprintf(1, 'WILL WORK ON SUBJECTS\n'); - disp(group(iGroup).subNumber); - end + fprintf(1, 'WILL WORK ON SUBJECTS\n'); + disp(opt.subjects); end diff --git a/src/getSpecificSubjects.m b/src/getSpecificSubjects.m deleted file mode 100644 index 0314deaa..00000000 --- a/src/getSpecificSubjects.m +++ /dev/null @@ -1,61 +0,0 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - -function group = getSpecificSubjects(opt, group, iGroup, subjects) - % - % Short description of what the function does goes here. - % - % USAGE:: - % - % [argout1, argout2] = templateFunction(argin1, [argin2 == default,] [argin3]) - % - % :param opt: Options chosen for the analysis. See ``checkOptions()``. - % :type opt: structure - % :param argin2: optional argument and its default value. And some of the - % options can be shown in litteral like ``this`` or ``that``. - % :type argin2: string - % :param argin3: (dimension) optional argument - % - % :returns: - :argout1: (type) (dimension) - % - :argout2: (type) (dimension) - % - % add a test for ata set with subject only blind02 and we ask for this one - % specifically - - % if no group or subject was specified we take all of them - if numel(opt.groups) == 1 && ... - strcmp(group(iGroup).name, '') && ... - isempty(opt.subjects{iGroup}) - - group(iGroup).subNumber = subjects; - - % if subject ID were directly specified by users we take those - elseif strcmp(group(iGroup).name, '') && iscellstr(opt.subjects) - - group(iGroup).subNumber = opt.subjects; - - % if group was specified we figure out which subjects to take - elseif ~isempty(opt.subjects{iGroup}) - - idx = opt.subjects{iGroup}; - - % else we take all subjects of that group - elseif isempty(opt.subjects{iGroup}) - - % count how many subjects in that group - idx = sum(~cellfun(@isempty, strfind(subjects, group(iGroup).name))); - idx = 1:idx; - - else - - error('Not sure what to do.'); - - end - - % if only indices were specified we get the subject from that group with that - if exist('idx', 'var') - pattern = [group(iGroup).name '%0' num2str(opt.zeropad) '.0f_']; - temp = strsplit(sprintf(pattern, idx), '_'); - group(iGroup).subNumber = temp(1:end - 1); - end - -end diff --git a/src/utils/getSubjectList.m b/src/utils/getSubjectList.m index e97336fc..75d05ef1 100644 --- a/src/utils/getSubjectList.m +++ b/src/utils/getSubjectList.m @@ -1,6 +1,43 @@ % (C) Copyright 2021 CPP BIDS SPM-pipeline developers function opt = getSubjectList(BIDS, opt) + % + % Returns the subjects to analyze in ``opt.subjects`` + % + % USAGE:: + % + % opt = getSubjectList(BIDS, opt) + % + % :param opt: Options chosen for the analysis. See ``checkOptions()``. + % :type opt: structure + % :param BIDSdir: the directory where the data is ; default is : + % ``fullfile(opt.dataDir, '..', 'derivatives', 'cpp_spm')`` + % :type BIDSdir: string + % + % :returns: + % - :opt: (structure) + % + % To set set the groups of subjects to analyze:: + % + % opt.groups = {'control', 'blind'}; + % + % If there are no groups (i.e subjects names are of the form ``sub-01`` for + % example) or if you want to run all subjects of all groups then use:: + % + % opt.groups = {''}; + % opt.subjects = {[]}; + % + % If you have more than 2 groups but want to only run the subjects of 2 + % groups then you can use:: + % + % opt.groups = {'cont', 'cat'}; + % opt.subjects = {[], []}; + % + % You can also directly specify the subject label for the participants you + % want to run:: + % + % opt.groups = {''}; + % opt.subjects = {'01', 'cont01', 'cat02', 'ctrl02', 'blind01'}; allSubjects = bids.query(BIDS, 'subjects'); diff --git a/tests/dummyData/derivatives/cpp_spm/participants.tsv b/tests/dummyData/derivatives/cpp_spm/participants.tsv index 0672bef8..6db63977 100755 --- a/tests/dummyData/derivatives/cpp_spm/participants.tsv +++ b/tests/dummyData/derivatives/cpp_spm/participants.tsv @@ -1,6 +1,7 @@ -participant_id Sex Age Educational level Smoker Medication Handedness -sub-01 1 66 14 0 1 30 -sub-02 1 28 16 1 0 12 -sub-ctrl01 0 61 16 0 0 18 - - +participant_id Sex Group Age Educational level Smoker Medication Handedness +sub-01 1 n/a 66 14 0 1 30 +sub-02 0 blind 28 16 1 0 12 +sub-ctrl01 1 ctrl 61 16 0 0 18 +sub-ctrl02 1 ctrl 45 16 0 0 18 +sub-blind01 1 blind 12 16 0 0 18 +sub-blind02 0 blind 61 16 0 0 18 diff --git a/tests/test_getData.m b/tests/test_getData.m index 7f4a07f4..105718c9 100644 --- a/tests/test_getData.m +++ b/tests/test_getData.m @@ -7,58 +7,9 @@ end function test_getDataBasic() - % Small test to ensure that getData returns what we asked for opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); opt.taskName = 'vismotion'; - opt.zeropad = 2; - - %% Get all groups all subjects - opt.groups = {''}; - opt.subjects = {[]}; - - [group] = getData(opt); - - assert(isequal(group(1).name, '')); - assert(isequal(group.numSub, 6)); - assert(isequal(group.subNumber, ... - {'01' '02' 'blind01' 'blind02' 'ctrl01' 'ctrl02'})); - - %% Get some subjects of some groups - opt.groups = {'ctrl', 'blind'}; - opt.subjects = {[1 2], 2}; - - [group] = getData(opt); - - assert(isequal(group(1).name, 'ctrl')); - assert(isequal(group(1).numSub, 2)); - assert(isequal(group(1).subNumber, {'ctrl01' 'ctrl02'})); - assert(isequal(group(2).name, 'blind')); - assert(isequal(group(2).numSub, 1)); - assert(isequal(group(2).subNumber, {'blind02'})); - - %% Get all subjects of some groups - opt.groups = {'ctrl', 'blind'}; - opt.subjects = {[], []}; - - [group] = getData(opt); - - assert(isequal(group(1).name, 'ctrl')); - assert(isequal(group(1).numSub, 2)); - assert(isequal(group(1).subNumber, {'ctrl01' 'ctrl02'})); - assert(isequal(group(2).name, 'blind')); - assert(isequal(group(2).numSub, 2)); - assert(isequal(group(2).subNumber, {'blind01' 'blind02'})); - - %% Get some specified subjects - opt.groups = {''}; - opt.subjects = {'01', 'ctrl02', 'blind02'}; - - [group] = getData(opt); - - assert(isequal(group(1).name, '')); - assert(isequal(group(1).numSub, 3)); - assert(isequal(group(1).subNumber, {'01', 'ctrl02', 'blind02'})); %% Only get anat metadata opt.groups = {''}; @@ -87,19 +38,3 @@ function test_getDataErrorTask() 'getData:noMatchingTask'); end - -function test_getDataErrorSubject() - % Small test to ensure that getData returns what we asked for - - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'vismotion'; - - %% Get all groups all subjects - opt.groups = {''}; - opt.subjects = {'03'}; - - assertExceptionThrown( ... - @()getData(opt), ... - 'getData:noMatchingSubject'); - -end From bd1e84b88ea26838539b12e5197f12d3255007ca Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Tue, 9 Mar 2021 10:30:39 +0100 Subject: [PATCH 009/145] remove call to groups in workflows and batches --- src/batches/setBatchFactorialDesign.m | 12 ++-- src/batches/setBatchMeanAnatAndMask.m | 19 +++---- src/batches/setBatchSmoothConImages.m | 31 ++++------ src/utils/printProcessingSubject.m | 7 +-- src/workflows/bidsConcatBetaTmaps.m | 60 +++++++++----------- src/workflows/bidsCopyRawFolder.m | 70 +++++++++++------------ src/workflows/bidsCreateVDM.m | 50 ++++++++--------- src/workflows/bidsFFX.m | 81 ++++++++++++--------------- src/workflows/bidsRFX.m | 4 +- src/workflows/bidsRealignReslice.m | 35 +++++------- src/workflows/bidsRealignUnwarp.m | 35 +++++------- src/workflows/bidsResliceTpmToFunc.m | 60 +++++++++----------- src/workflows/bidsResults.m | 37 ++++++------ src/workflows/bidsSTC.m | 20 +++---- src/workflows/bidsSegmentSkullStrip.m | 29 ++++------ src/workflows/bidsSmoothing.m | 20 +++---- src/workflows/bidsSpatialPrepro.m | 70 ++++++++++------------- src/workflows/saveAndRunWorkflow.m | 6 +- src/workflows/setUpWorkflow.m | 4 +- 19 files changed, 280 insertions(+), 370 deletions(-) diff --git a/src/batches/setBatchFactorialDesign.m b/src/batches/setBatchFactorialDesign.m index f183b9d4..c766a6b8 100644 --- a/src/batches/setBatchFactorialDesign.m +++ b/src/batches/setBatchFactorialDesign.m @@ -28,7 +28,7 @@ smoothPrefix = ['s', num2str(conFWHM)]; end - [group, opt] = getData(opt); + [~, opt] = getData(opt); rfxDir = getRFXdir(opt, funcFWHM, conFWHM); @@ -55,20 +55,18 @@ mkdir(directory); % For each group - for iGroup = 1:length(group) - - groupName = group(iGroup).name; + for iGroup = 1:length(opt.groups) icell(iGroup).levels = iGroup; %#ok<*AGROW> for iSub = 1:group(iGroup).numSub - subID = group(iGroup).subNumber{iSub}; + subLabel = group(iGroup).subNumber{iSub}; - printProcessingSubject(groupName, iSub, subID); + printProcessingSubject(iSub, subLabel); % FFX directory and load SPM.mat of that subject - ffxDir = getFFXdir(subID, funcFWHM, opt); + ffxDir = getFFXdir(subLabel, funcFWHM, opt); load(fullfile(ffxDir, 'SPM.mat')); % find which contrast of that subject has the name of the contrast we diff --git a/src/batches/setBatchMeanAnatAndMask.m b/src/batches/setBatchMeanAnatAndMask.m index bd845192..99240062 100644 --- a/src/batches/setBatchMeanAnatAndMask.m +++ b/src/batches/setBatchMeanAnatAndMask.m @@ -2,7 +2,7 @@ function matlabbatch = setBatchMeanAnatAndMask(matlabbatch, opt, funcFWHM, outputDir) % - % Creates batxh to create mean anatomical image and a grop mask + % Creates batxh to create mean anatomical image and a group mask % % USAGE:: % @@ -19,25 +19,21 @@ % :returns: - :matlabbatch: (structure) % - [group, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); printBatchName('create mean anatomical image and mask'); inputAnat = {}; inputMask = {}; - for iGroup = 1:length(group) + for iSub = 1:numel(opt.subjects) - groupName = group(iGroup).name; + subLabel = opt.subjects{iSub}; - for iSub = 1:group(iGroup).numSub - - subID = group(iGroup).subNumber{iSub}; - - printProcessingSubject(groupName, iSub, subID); + printProcessingSubject(iSub, subLabel); %% Anat - [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); + [anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt); anatImage = validationInputFile( ... anatDataDir, ... @@ -48,13 +44,12 @@ inputAnat{end + 1, 1} = anatImage; %#ok<*AGROW> %% Mask - ffxDir = getFFXdir(subID, funcFWHM, opt); + ffxDir = getFFXdir(subLabel, funcFWHM, opt); files = validationInputFile(ffxDir, 'mask.nii'); inputMask{end + 1, 1} = files; - end end %% Generate the equation to get the mean of the mask and structural image diff --git a/src/batches/setBatchSmoothConImages.m b/src/batches/setBatchSmoothConImages.m index 8e19b61c..9e8d6dd0 100644 --- a/src/batches/setBatchSmoothConImages.m +++ b/src/batches/setBatchSmoothConImages.m @@ -1,6 +1,6 @@ % (C) Copyright 2019 CPP BIDS SPM-pipeline developers -function matlabbatch = setBatchSmoothConImages(matlabbatch, group, opt, funcFWHM, conFWHM) +function matlabbatch = setBatchSmoothConImages(matlabbatch, opt, funcFWHM, conFWHM) % % Creates a batch to smooth all the con images of all subjects % @@ -24,29 +24,22 @@ printBatchName('smoothing contrast images'); - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) + for iSub = 1:numel(opt.subjects) - groupName = group(iGroup).name; + subLabel = opt.subjects{iSub}; - for iSub = 1:group(iGroup).numSub + printProcessingSubject(iSub, subLabel); - subNumber = group(iGroup).subNumber{iSub}; + ffxDir = getFFXdir(subLabel, funcFWHM, opt); - printProcessingSubject(groupName, iSub, subNumber); + conImg = spm_select('FPlist', ffxDir, '^con*.*nii$'); + data = cellstr(conImg); - ffxDir = getFFXdir(subNumber, funcFWHM, opt); - - conImg = spm_select('FPlist', ffxDir, '^con*.*nii$'); - data = cellstr(conImg); - - matlabbatch = setBatchSmoothing( ... - matlabbatch, ... - data, ... - conFWHM, ... - [spm_get_defaults('smooth.prefix'), num2str(conFWHM)]); - - end + matlabbatch = setBatchSmoothing( ... + matlabbatch, ... + data, ... + conFWHM, ... + [spm_get_defaults('smooth.prefix'), num2str(conFWHM)]); end diff --git a/src/utils/printProcessingSubject.m b/src/utils/printProcessingSubject.m index cb5a27a5..a0668d42 100644 --- a/src/utils/printProcessingSubject.m +++ b/src/utils/printProcessingSubject.m @@ -1,11 +1,10 @@ % (C) Copyright 2019 CPP BIDS SPM-pipeline developers -function printProcessingSubject(groupName, iSub, subID) +function printProcessingSubject(iSub, subLabel) fprintf(1, [ ... - ' PROCESSING GROUP: %s' ... - 'SUBJECT No.: %i ' ... + ' PROCESSING SUBJECT No.: %i ' ... 'SUBJECT ID : %s \n'], ... - groupName, iSub, subID); + iSub, subLabel); end diff --git a/src/workflows/bidsConcatBetaTmaps.m b/src/workflows/bidsConcatBetaTmaps.m index b2a6ff46..dc757327 100644 --- a/src/workflows/bidsConcatBetaTmaps.m +++ b/src/workflows/bidsConcatBetaTmaps.m @@ -22,55 +22,51 @@ function bidsConcatBetaTmaps(opt, funcFWHM, deleteIndBeta, deleteIndTmaps) deleteIndTmaps = 1; end - [~, opt, group] = setUpWorkflow(opt, 'merge beta images and t-maps'); + [~, opt] = setUpWorkflow(opt, 'merge beta images and t-maps'); - % clear previous matlabbatch and files - matlabbatch = []; RT = 0; - %% Loop through the groups, subjects - for iGroup = 1:length(group) + for iSub = 1:numel(opt.subjects) - for iSub = 1:group(iGroup).numSub + subLabel = opt.subjects{iSub}; - subID = group(iGroup).subNumber{iSub}; + printProcessingSubject(iSub, subLabel); - ffxDir = getFFXdir(subID, funcFWHM, opt); + ffxDir = getFFXdir(subLabel, funcFWHM, opt); - contrasts = specifyContrasts(ffxDir, opt.taskName, opt); + contrasts = specifyContrasts(ffxDir, opt.taskName, opt); - beta_maps = cell(length(contrasts), 1); - t_maps = cell(length(contrasts), 1); + beta_maps = cell(length(contrasts), 1); + t_maps = cell(length(contrasts), 1); - % path to beta and t-map files. - for iContrast = 1:length(beta_maps) - % Note that the betas are created from the idx (Beta_idx(iBeta)) - fileName = sprintf('beta_%04d.nii', find(contrasts(iContrast).C)); - fileName = validationInputFile(ffxDir, fileName); - beta_maps{iContrast, 1} = [fileName, ',1']; + % path to beta and t-map files. + for iContrast = 1:length(beta_maps) + % Note that the betas are created from the idx (Beta_idx(iBeta)) + fileName = sprintf('beta_%04d.nii', find(contrasts(iContrast).C)); + fileName = validationInputFile(ffxDir, fileName); + beta_maps{iContrast, 1} = [fileName, ',1']; - % while the contrastes (t-maps) are not from the index. They were created - fileName = sprintf('spmT_%04d.nii', iContrast); - fileName = validationInputFile(ffxDir, fileName); - t_maps{iContrast, 1} = [fileName, ',1']; - end + % while the contrastes (t-maps) are not from the index. They were created + fileName = sprintf('spmT_%04d.nii', iContrast); + fileName = validationInputFile(ffxDir, fileName); + t_maps{iContrast, 1} = [fileName, ',1']; + end - % beta maps - outputName = ['4D_beta_', num2str(funcFWHM), '.nii']; + % beta maps + outputName = ['4D_beta_', num2str(funcFWHM), '.nii']; - matlabbatch = []; - matlabbatch = setBatch3Dto4D(matlabbatch, beta_maps, RT, outputName); + matlabbatch = []; + matlabbatch = setBatch3Dto4D(matlabbatch, beta_maps, RT, outputName); - % t-maps - outputName = ['4D_t_maps_', num2str(funcFWHM), '.nii']; + % t-maps + outputName = ['4D_t_maps_', num2str(funcFWHM), '.nii']; - matlabbatch = setBatch3Dto4D(matlabbatch, t_maps, RT, outputName); + matlabbatch = setBatch3Dto4D(matlabbatch, t_maps, RT, outputName); - saveAndRunWorkflow(matlabbatch, 'concat_betaImg_tMaps', opt, subID); + saveAndRunWorkflow(matlabbatch, 'concat_betaImg_tMaps', opt, subLabel); - removeBetaImgTmaps(t_maps, deleteIndBeta, deleteIndTmaps, ffxDir); + removeBetaImgTmaps(t_maps, deleteIndBeta, deleteIndTmaps, ffxDir); - end end end diff --git a/src/workflows/bidsCopyRawFolder.m b/src/workflows/bidsCopyRawFolder.m index f59a6391..de692872 100644 --- a/src/workflows/bidsCopyRawFolder.m +++ b/src/workflows/bidsCopyRawFolder.m @@ -66,54 +66,50 @@ function bidsCopyRawFolder(opt, deleteZippedNii, modalitiesToCopy, unZip) copyTsvJson(rawDir, derivativesDir); %% Loop through the groups, subjects, sessions - [group, opt, BIDS] = getData(opt, rawDir); + [BIDS, opt] = getData(opt, rawDir); - for iGroup = 1:length(group) + for iSub = 1:numel(opt.subjects) - for iSub = 1:group(iGroup).numSub + subLabel = opt.subjects{iSub}; - subID = group(iGroup).subNumber{iSub}; + subDir = returnSubjectDir(subLabel); - subDir = returnSubjectDir(subID); + fprintf('copying subject: %s \n', subDir); - fprintf('copying subject: %s \n', subDir); + [~, ~, ~] = mkdir(fullfile(derivativesDir, subDir)); - [~, ~, ~] = mkdir(fullfile(derivativesDir, subDir)); + % copy scans.tsv files + copyTsvJson( ... + fullfile(rawDir, subDir), ... + fullfile(derivativesDir, subDir)); - % copy scans.tsv files - copyTsvJson( ... - fullfile(rawDir, subDir), ... - fullfile(derivativesDir, subDir)); + [sessions, nbSessions] = getInfo(BIDS, subLabel, opt, 'Sessions'); - [sessions, nbSessions] = getInfo(BIDS, subID, opt, 'Sessions'); + %% copy the whole subject's folder + % use a call to system cp function to use the derefence option (-L) + % to get the data 'out' of an eventual datalad dataset - %% copy the whole subject's folder - % use a call to system cp function to use the derefence option (-L) - % to get the data 'out' of an eventual datalad dataset + for iSes = 1:nbSessions - for iSes = 1:nbSessions + sessionDir = returnSessionDir(sessions{iSes}); - sessionDir = returnSessionDir(sessions{iSes}); + fprintf(' copying session: %s \n', sessionDir); - fprintf(' copying session: %s \n', sessionDir); + [~, ~, ~] = mkdir(fullfile(derivativesDir, subDir, sessionDir)); - [~, ~, ~] = mkdir(fullfile(derivativesDir, subDir, sessionDir)); - - % copy scans.tsv files - copyTsvJson( ... - fullfile(rawDir, subDir, sessionDir), ... - fullfile(derivativesDir, subDir, sessionDir)); + % copy scans.tsv files + copyTsvJson( ... + fullfile(rawDir, subDir, sessionDir), ... + fullfile(derivativesDir, subDir, sessionDir)); - modalities = bids.query(BIDS, 'modalities', ... - 'sub', subID, ... - 'ses', sessions{iSes}); - modalities = intersect(modalities, modalitiesToCopy); + modalities = bids.query(BIDS, 'modalities', ... + 'sub', subLabel, ... + 'ses', sessions{iSes}); + modalities = intersect(modalities, modalitiesToCopy); - copyModalities(BIDS, opt, modalities, subID, sessions{iSes}); + copyModalities(BIDS, opt, modalities, subLabel, sessions{iSes}); - end end - end if unZip @@ -131,9 +127,9 @@ function bidsCopyRawFolder(opt, deleteZippedNii, modalitiesToCopy, unZip) end -function subDir = returnSubjectDir(subID) +function subDir = returnSubjectDir(subLabel) - subDir = ['sub-', subID]; + subDir = ['sub-', subLabel]; end @@ -164,11 +160,11 @@ function copyTsvJson(srcDir, targetDir) end -function copyModalities(BIDS, opt, modalities, subID, session) +function copyModalities(BIDS, opt, modalities, subLabel, session) [rawDir, derivativesDir] = returnRawAndDerivativeDir(opt); - subDir = returnSubjectDir(subID); + subDir = returnSubjectDir(subLabel); sessionDir = returnSessionDir(session); @@ -189,7 +185,7 @@ function copyModalities(BIDS, opt, modalities, subID, session) if strcmp(modalities{iModality}, 'func') files = bids.query(BIDS, 'data', ... - 'sub', subID, ... + 'sub', subLabel, ... 'ses', session, ... 'task', opt.taskName); @@ -254,7 +250,7 @@ function unzipFiles(derivativesDir, deleteZippedNii, opt) % for bold, physio and stim files, we only unzip the files of the task of % interest if any(strcmp(fragments.type, {'bold', 'stim', 'physio'})) && ... - isfield(fragments, 'task') && strcmp(fragments.task, opt.taskName) + isfield(fragments, 'task') && strcmp(fragments.task, opt.taskName) % load the nifti image and saves the functional data as unzipped nii n = load_untouch_nii(file); diff --git a/src/workflows/bidsCreateVDM.m b/src/workflows/bidsCreateVDM.m index b06b1e7e..f3a83ec6 100644 --- a/src/workflows/bidsCreateVDM.m +++ b/src/workflows/bidsCreateVDM.m @@ -25,45 +25,41 @@ function bidsCreateVDM(opt) opt = []; end - [BIDS, opt, group] = setUpWorkflow(opt, 'create voxel displacement map'); + [BIDS, opt] = setUpWorkflow(opt, 'create voxel displacement map'); - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) + parfor iSub = 1:numel(opt.subjects) - groupName = group(iGroup).name; + subLabel = opt.subjects{iSub}; - parfor iSub = 1:group(iGroup).numSub + printProcessingSubject(iSub, subLabel); - subID = group(iGroup).subNumber{iSub}; + % TODO Move to getInfo + types = bids.query(BIDS, 'types', 'sub', subLabel); - % TODO Move to getInfo - types = bids.query(BIDS, 'types', 'sub', subID); + if any(ismember(types, {'phase12', 'phasediff', 'fieldmap', 'epi'})) - if any(ismember(types, {'phase12', 'phasediff', 'fieldmap', 'epi'})) + printProcessingSubject(groupName, iSub, subLabel); - printProcessingSubject(groupName, iSub, subID); + % Create rough mean of the 1rst run to improve SNR for coregistration + % TODO use the slice timed EPI if STC was used ? + sessions = getInfo(BIDS, subLabel, opt, 'Sessions'); + runs = getInfo(BIDS, subLabel, opt, 'Runs', sessions{1}); + [fileName, subFuncDataDir] = getBoldFilename(BIDS, subLabel, sessions{1}, runs{1}, opt); + spmup_basics(fullfile(subFuncDataDir, fileName), 'mean'); - % Create rough mean of the 1rst run to improve SNR for coregistration - % TODO use the slice timed EPI if STC was used ? - sessions = getInfo(BIDS, subID, opt, 'Sessions'); - runs = getInfo(BIDS, subID, opt, 'Runs', sessions{1}); - [fileName, subFuncDataDir] = getBoldFilename(BIDS, subID, sessions{1}, runs{1}, opt); - spmup_basics(fullfile(subFuncDataDir, fileName), 'mean'); + matlabbatch = []; + matlabbatch = setBatchCoregistrationFmap(matlabbatch, BIDS, opt, subLabel); + saveAndRunWorkflow(matlabbatch, 'coregister_fmap', opt, subLabel); - matlabbatch = []; - matlabbatch = setBatchCoregistrationFmap(matlabbatch, BIDS, opt, subID); - saveAndRunWorkflow(matlabbatch, 'coregister_fmap', opt, subID); + matlabbatch = []; + matlabbatch = setBatchCreateVDMs(matlabbatch, BIDS, opt, subLabel); + saveAndRunWorkflow(matlabbatch, 'create_vdm', opt, subLabel); - matlabbatch = []; - matlabbatch = setBatchCreateVDMs(matlabbatch, BIDS, opt, subID); - saveAndRunWorkflow(matlabbatch, 'create_vdm', opt, subID); - - % TODO - % delete temporary mean images ?? - - end + % TODO + % delete temporary mean images ?? end end + end diff --git a/src/workflows/bidsFFX.m b/src/workflows/bidsFFX.m index 3ad26a53..34b2722e 100644 --- a/src/workflows/bidsFFX.m +++ b/src/workflows/bidsFFX.m @@ -32,70 +32,63 @@ function bidsFFX(action, opt, funcFWHM) opt = []; end - [BIDS, opt, group] = setUpWorkflow(opt, 'subject level GLM'); + [BIDS, opt] = setUpWorkflow(opt, 'subject level GLM'); if isempty(opt.model.file) opt = createDefaultModel(BIDS, opt); end - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) + for iSub = 1:numel(opt.subjects) - groupName = group(iGroup).name; + subLabel = opt.subjects{iSub}; - for iSub = 1:group(iGroup).numSub + printProcessingSubject(iSub, subLabel); - subID = group(iGroup).subNumber{iSub}; + matlabbatch = []; - printProcessingSubject(groupName, iSub, subID); + switch action - matlabbatch = []; + case 'specifyAndEstimate' - switch action + matlabbatch = setBatchSubjectLevelGLMSpec(matlabbatch, BIDS, opt, subLabel, funcFWHM); + matlabbatch = setBatchPrintFigure(matlabbatch, [ ... + 'sub-', subLabel, ... + '_task-', opt.taskName, ... + '_design_before_estimation']); + matlabbatch = setBatchEstimateModel(matlabbatch, opt); + matlabbatch = setBatchPrintFigure(matlabbatch, [ ... + 'sub-', subLabel, ... + '_task-', opt.taskName, ... + '_design_after_estimation']); - case 'specifyAndEstimate' + batchName = ... + ['specify_estimate_ffx_task-', opt.taskName, ... + '_space-', opt.space, ... + '_FWHM-', num2str(funcFWHM)]; - matlabbatch = setBatchSubjectLevelGLMSpec(matlabbatch, BIDS, opt, subID, funcFWHM); - matlabbatch = setBatchPrintFigure(matlabbatch, [ ... - 'sub-', subID, ... - '_task-', opt.taskName, ... - '_design_before_estimation']); - matlabbatch = setBatchEstimateModel(matlabbatch, opt); - matlabbatch = setBatchPrintFigure(matlabbatch, [ ... - 'sub-', subID, ... - '_task-', opt.taskName, ... - '_design_after_estimation']); + saveAndRunWorkflow(matlabbatch, batchName, opt, subLabel); - batchName = ... - ['specify_estimate_ffx_task-', opt.taskName, ... - '_space-', opt.space, ... - '_FWHM-', num2str(funcFWHM)]; + if opt.glmQA.do + plot_power_spectra_of_GLM_residuals( ... + getFFXdir(subLabel, funcFWHM, opt), ... + opt.metadata.RepetitionTime); - saveAndRunWorkflow(matlabbatch, batchName, opt, subID); + deleteResidualImages(getFFXdir(subLabel, funcFWHM, opt)); - if opt.glmQA.do - plot_power_spectra_of_GLM_residuals( ... - getFFXdir(subID, funcFWHM, opt), ... - opt.metadata.RepetitionTime); + movefile(['sub-', subLabel, '_task-', opt.taskName, '_design_*'], ... + getFFXdir(subLabel, funcFWHM, opt)); + end - deleteResidualImages(getFFXdir(subID, funcFWHM, opt)); + case 'contrasts' - movefile(['sub-', subID, '_task-', opt.taskName, '_design_*'], ... - getFFXdir(subID, funcFWHM, opt)); - end + matlabbatch = setBatchSubjectLevelContrasts(matlabbatch, opt, subLabel, funcFWHM); - case 'contrasts' + batchName = ... + ['contrasts_ffx_task-', opt.taskName, ... + '_space-', opt.space, ... + '_FWHM-', num2str(funcFWHM)]; - matlabbatch = setBatchSubjectLevelContrasts(matlabbatch, opt, subID, funcFWHM); - - batchName = ... - ['contrasts_ffx_task-', opt.taskName, ... - '_space-', opt.space, ... - '_FWHM-', num2str(funcFWHM)]; - - saveAndRunWorkflow(matlabbatch, batchName, opt, subID); - - end + saveAndRunWorkflow(matlabbatch, batchName, opt, subLabel); end diff --git a/src/workflows/bidsRFX.m b/src/workflows/bidsRFX.m index c4990bec..f86a26e7 100644 --- a/src/workflows/bidsRFX.m +++ b/src/workflows/bidsRFX.m @@ -43,14 +43,14 @@ function bidsRFX(action, opt, funcFWHM, conFWHM) conFWHM = 0; end - [~, opt, group] = setUpWorkflow(opt, 'group level GLM'); + [~, opt] = setUpWorkflow(opt, 'group level GLM'); switch action case 'smoothContrasts' matlabbatch = []; - matlabbatch = setBatchSmoothConImages(matlabbatch, group, opt, funcFWHM, conFWHM); + matlabbatch = setBatchSmoothConImages(matlabbatch, opt, funcFWHM, conFWHM); saveAndRunWorkflow(matlabbatch, ... ['smooth_con_FWHM-', num2str(conFWHM), '_task-', opt.taskName], ... diff --git a/src/workflows/bidsRealignReslice.m b/src/workflows/bidsRealignReslice.m index 66f46cbf..3ab69d17 100644 --- a/src/workflows/bidsRealignReslice.m +++ b/src/workflows/bidsRealignReslice.m @@ -19,35 +19,26 @@ function bidsRealignReslice(opt) opt = []; end - [BIDS, opt, group] = setUpWorkflow(opt, 'realign and reslice'); + [BIDS, opt] = setUpWorkflow(opt, 'realign and reslice'); - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) + parfor iSub = 1:numel(opt.subjects) - groupName = group(iGroup).name; + subLabel = opt.subjects{iSub}; - parfor iSub = 1:group(iGroup).numSub + printProcessingSubject(iSub, subLabel); - % Get the ID of the subject - % (i.e SubNumber doesnt have to match the iSub if one subject - % is exluded for any reason) - subID = group(iGroup).subNumber{iSub}; % Get the subject ID + matlabbatch = []; + [matlabbatch, ~] = setBatchRealign( ... + matlabbatch, ... + BIDS, ... + subLabel, ... + opt, ... + 'realignReslice'); - printProcessingSubject(groupName, iSub, subID); + saveAndRunWorkflow(matlabbatch, 'realign_reslice', opt, subLabel); - matlabbatch = []; - [matlabbatch, ~] = setBatchRealign( ... - matlabbatch, ... - BIDS, ... - subID, ... - opt, ... - 'realignReslice'); + copyFigures(BIDS, opt, subLabel); - saveAndRunWorkflow(matlabbatch, 'realign_reslice', opt, subID); - - copyFigures(BIDS, opt, subID); - - end end end diff --git a/src/workflows/bidsRealignUnwarp.m b/src/workflows/bidsRealignUnwarp.m index e8db8d0b..fc299f3a 100644 --- a/src/workflows/bidsRealignUnwarp.m +++ b/src/workflows/bidsRealignUnwarp.m @@ -22,35 +22,26 @@ function bidsRealignUnwarp(opt) opt = []; end - [BIDS, opt, group] = setUpWorkflow(opt, 'realign and unwarp'); + [BIDS, opt] = setUpWorkflow(opt, 'realign and unwarp'); - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) + parfor iSub = 1:numel(opt.subjects) - groupName = group(iGroup).name; + subLabel = opt.subjects{iSub}; - parfor iSub = 1:group(iGroup).numSub + printProcessingSubject(iSub, subLabel); - % Get the ID of the subject - % (i.e SubNumber doesnt have to match the iSub if one subject - % is exluded for any reason) - subID = group(iGroup).subNumber{iSub}; % Get the subject ID + matlabbatch = []; + [matlabbatch, ~] = setBatchRealign( ... + matlabbatch, ... + BIDS, ... + subLabel, ... + opt, ... + 'realignUnwarp'); - printProcessingSubject(groupName, iSub, subID); + saveAndRunWorkflow(matlabbatch, 'realign_unwarp', opt, subLabel); - matlabbatch = []; - [matlabbatch, ~] = setBatchRealign( ... - matlabbatch, ... - BIDS, ... - subID, ... - opt, ... - 'realignUnwarp'); + copyFigures(BIDS, opt, subLabel); - saveAndRunWorkflow(matlabbatch, 'realign_unwarp', opt, subID); - - copyFigures(BIDS, opt, subID); - - end end end diff --git a/src/workflows/bidsResliceTpmToFunc.m b/src/workflows/bidsResliceTpmToFunc.m index 0421e856..561f3398 100644 --- a/src/workflows/bidsResliceTpmToFunc.m +++ b/src/workflows/bidsResliceTpmToFunc.m @@ -25,52 +25,44 @@ function bidsResliceTpmToFunc(opt) opt = []; end - [BIDS, opt, group] = setUpWorkflow(opt, ... - 'reslicing tissue probability maps to functional dimension'); + [BIDS, opt] = setUpWorkflow(opt, 'reslicing tissue probability maps to functional dimension'); - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) + for iSub = 1:numel(opt.subjects) - groupName = group(iGroup).name; + subLabel = opt.subjects{iSub}; - for iSub = 1:group(iGroup).numSub + printProcessingSubject(iSub, subLabel); - subID = group(iGroup).subNumber{iSub}; + [meanImage, meanFuncDir] = getMeanFuncFilename(BIDS, subLabel, opt); - printProcessingSubject(groupName, iSub, subID); + % get grey and white matter and CSF tissue probability maps + [anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt); + TPMs = validationInputFile(anatDataDir, anatImage, 'c[123]'); - [meanImage, meanFuncDir] = getMeanFuncFilename(BIDS, subID, opt); + matlabbatch = []; + matlabbatch = setBatchReslice(matlabbatch, ... + fullfile(meanFuncDir, meanImage), ... + cellstr(TPMs)); - % get grey and white matter and CSF tissue probability maps - [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); - TPMs = validationInputFile(anatDataDir, anatImage, 'c[123]'); + saveAndRunWorkflow(matlabbatch, 'reslice_tpm', opt, subLabel); - matlabbatch = []; - matlabbatch = setBatchReslice(matlabbatch, ... - fullfile(meanFuncDir, meanImage), ... - cellstr(TPMs)); + %% Compute brain mask of functional + TPMs = validationInputFile(anatDataDir, anatImage, 'rc[123]'); + % greay matter + input{1, 1} = TPMs(1, :); + % white matter + input{2, 1} = TPMs(2, :); + % csf + input{3, 1} = TPMs(3, :); - saveAndRunWorkflow(matlabbatch, 'reslice_tpm', opt, subID); + output = strrep(meanImage, '.nii', '_mask.nii'); - %% Compute brain mask of functional - TPMs = validationInputFile(anatDataDir, anatImage, 'rc[123]'); - % greay matter - input{1, 1} = TPMs(1, :); - % white matter - input{2, 1} = TPMs(2, :); - % csf - input{3, 1} = TPMs(3, :); + expression = sprintf('(i1+i2+i3)>%f', opt.skullstrip.threshold); - output = strrep(meanImage, '.nii', '_mask.nii'); + matlabbatch = []; + matlabbatch = setBatchImageCalculation(matlabbatch, input, output, meanFuncDir, expression); - expression = sprintf('(i1+i2+i3)>%f', opt.skullstrip.threshold); - - matlabbatch = []; - matlabbatch = setBatchImageCalculation(matlabbatch, input, output, meanFuncDir, expression); - - saveAndRunWorkflow(matlabbatch, 'create_functional_brain_mask', opt, subID); - - end + saveAndRunWorkflow(matlabbatch, 'create_functional_brain_mask', opt, subLabel); end diff --git a/src/workflows/bidsResults.m b/src/workflows/bidsResults.m index 05c00a88..8b129d5e 100644 --- a/src/workflows/bidsResults.m +++ b/src/workflows/bidsResults.m @@ -25,7 +25,7 @@ function bidsResults(opt, funcFWHM, conFWHM) opt = []; end - [~, opt, group] = setUpWorkflow(opt, 'computing GLM results'); + [~, opt] = setUpWorkflow(opt, 'computing GLM results'); matlabbatch = []; @@ -47,34 +47,29 @@ function bidsResults(opt, funcFWHM, conFWHM) case 'subject' - for iGroup = 1:length(group) + % For each subject + for iSub = 1:numel(opt.subjects) - % For each subject - for iSub = 1:group(iGroup).numSub + subLabel = opt.subjects{iSub}; - for iCon = 1:length(opt.result.Steps(iStep).Contrasts) + for iCon = 1:length(opt.result.Steps(iStep).Contrasts) - % Get the Subject ID - subID = group(iGroup).subNumber{iSub}; - - matlabbatch = ... - setBatchSubjectLevelResults( ... - matlabbatch, ... - opt, ... - subID, ... - funcFWHM, ... - iStep, ... - iCon); - - end + matlabbatch = ... + setBatchSubjectLevelResults( ... + matlabbatch, ... + opt, ... + subLabel, ... + funcFWHM, ... + iStep, ... + iCon); end - batchName = sprintf('compute_sub-%s_results', subID); + end - saveAndRunWorkflow(matlabbatch, batchName, opt, subID); + batchName = sprintf('compute_sub-%s_results', subLabel); - end + saveAndRunWorkflow(matlabbatch, batchName, opt, subLabel); case 'dataset' diff --git a/src/workflows/bidsSTC.m b/src/workflows/bidsSTC.m index ade9c400..ee83b8e3 100644 --- a/src/workflows/bidsSTC.m +++ b/src/workflows/bidsSTC.m @@ -34,25 +34,19 @@ function bidsSTC(opt) opt = []; end - [BIDS, opt, group] = setUpWorkflow(opt, 'slice timing correction'); + [BIDS, opt] = setUpWorkflow(opt, 'slice timing correction'); - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) + parfor iSub = 1:numel(opt.subjects) - groupName = group(iGroup).name; + subLabel = opt.subjects{iSub}; - parfor iSub = 1:group(iGroup).numSub + printProcessingSubject(iSub, subLabel); - subID = group(iGroup).subNumber{iSub}; + matlabbatch = []; + matlabbatch = setBatchSTC(matlabbatch, BIDS, opt, subLabel); - printProcessingSubject(groupName, iSub, subID); + saveAndRunWorkflow(matlabbatch, 'STC', opt, subLabel); - matlabbatch = []; - matlabbatch = setBatchSTC(matlabbatch, BIDS, opt, subID); - - saveAndRunWorkflow(matlabbatch, 'STC', opt, subID); - - end end end diff --git a/src/workflows/bidsSegmentSkullStrip.m b/src/workflows/bidsSegmentSkullStrip.m index 1e2da98a..f29a3a7b 100644 --- a/src/workflows/bidsSegmentSkullStrip.m +++ b/src/workflows/bidsSegmentSkullStrip.m @@ -17,32 +17,27 @@ function bidsSegmentSkullStrip(opt) opt = []; end - [BIDS, opt, group] = setUpWorkflow(opt, 'segmentation and skulltripping'); + [BIDS, opt] = setUpWorkflow(opt, 'segmentation and skulltripping'); - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) + opt.orderBatches.selectAnat = 1; + opt.orderBatches.segment = 2; - groupName = group(iGroup).name; + parfor iSub = 1:numel(opt.subjects) - for iSub = 1:group(iGroup).numSub + subLabel = opt.subjects{iSub}; - subID = group(iGroup).subNumber{iSub}; + printProcessingSubject(iSub, subLabel); - printProcessingSubject(groupName, iSub, subID); + matlabbatch = []; + matlabbatch = setBatchSelectAnat(matlabbatch, BIDS, opt, subLabel); - matlabbatch = []; - matlabbatch = setBatchSelectAnat(matlabbatch, BIDS, opt, subID); - opt.orderBatches.selectAnat = 1; + % dependency from file selector ('Anatomical') + matlabbatch = setBatchSegmentation(matlabbatch, opt); - % dependency from file selector ('Anatomical') - matlabbatch = setBatchSegmentation(matlabbatch, opt); - opt.orderBatches.segment = 2; + matlabbatch = setBatchSkullStripping(matlabbatch, BIDS, subLabel, opt); - matlabbatch = setBatchSkullStripping(matlabbatch, BIDS, subID, opt); + saveAndRunWorkflow(matlabbatch, 'segment_skullstrip', opt, subLabel); - saveAndRunWorkflow(matlabbatch, 'segment_skullstrip', opt, subID); - - end end end diff --git a/src/workflows/bidsSmoothing.m b/src/workflows/bidsSmoothing.m index 2dd19d46..199d8d63 100644 --- a/src/workflows/bidsSmoothing.m +++ b/src/workflows/bidsSmoothing.m @@ -22,25 +22,19 @@ function bidsSmoothing(funcFWHM, opt) opt = []; end - [BIDS, opt, group] = setUpWorkflow(opt, 'smoothing functional data'); + [BIDS, opt] = setUpWorkflow(opt, 'smoothing functional data'); - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) + parfor iSub = 1:numel(opt.subjects) - groupName = group(iGroup).name; + subLabel = opt.subjects{iSub}; - parfor iSub = 1:group(iGroup).numSub + printProcessingSubject(iSub, subLabel); - subID = group(iGroup).subNumber{iSub}; + matlabbatch = []; + matlabbatch = setBatchSmoothingFunc(matlabbatch, BIDS, opt, subLabel, funcFWHM); - printProcessingSubject(groupName, iSub, subID); + saveAndRunWorkflow(matlabbatch, ['smoothing_FWHM-' num2str(funcFWHM)], opt, subLabel); - matlabbatch = []; - matlabbatch = setBatchSmoothingFunc(matlabbatch, BIDS, opt, subID, funcFWHM); - - saveAndRunWorkflow(matlabbatch, ['smoothing_FWHM-' num2str(funcFWHM)], opt, subID); - - end end end diff --git a/src/workflows/bidsSpatialPrepro.m b/src/workflows/bidsSpatialPrepro.m index 91e73a34..c29ac266 100644 --- a/src/workflows/bidsSpatialPrepro.m +++ b/src/workflows/bidsSpatialPrepro.m @@ -42,7 +42,7 @@ function bidsSpatialPrepro(opt) opt = []; end - [BIDS, opt, group] = setUpWorkflow(opt, 'spatial preprocessing'); + [BIDS, opt] = setUpWorkflow(opt, 'spatial preprocessing'); opt.orderBatches.selectAnat = 1; opt.orderBatches.realign = 2; @@ -52,59 +52,51 @@ function bidsSpatialPrepro(opt) opt.orderBatches.skullStripping = 6; opt.orderBatches.skullStrippingMask = 7; - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) + parfor iSub = 1:numel(opt.subjects) - groupName = group(iGroup).name; + matlabbatch = []; - parfor iSub = 1:group(iGroup).numSub + subLabel = opt.subjects{iSub}; - matlabbatch = []; - % Get the ID of the subject - % (i.e SubNumber doesnt have to match the iSub if one subject - % is exluded for any reason) - subID = group(iGroup).subNumber{iSub}; + printProcessingSubject(iSub, subLabel); - printProcessingSubject(groupName, iSub, subID); + matlabbatch = setBatchSelectAnat(matlabbatch, BIDS, opt, subLabel); - matlabbatch = setBatchSelectAnat(matlabbatch, BIDS, opt, subID); - - % if action is emtpy then only realign will be done - action = []; - if ~opt.realign.useUnwarp - action = 'realign'; - end - [matlabbatch, voxDim] = setBatchRealign(matlabbatch, action, BIDS, opt, subID); + % if action is emtpy then only realign will be done + action = []; + if ~opt.realign.useUnwarp + action = 'realign'; + end + [matlabbatch, voxDim] = setBatchRealign(matlabbatch, action, BIDS, opt, subLabel); - % dependency from file selector ('Anatomical') - matlabbatch = setBatchCoregistrationFuncToAnat(matlabbatch, BIDS, opt, subID); + % dependency from file selector ('Anatomical') + matlabbatch = setBatchCoregistrationFuncToAnat(matlabbatch, BIDS, opt, subLabel); - matlabbatch = setBatchSaveCoregistrationMatrix(matlabbatch, BIDS, opt, subID); + matlabbatch = setBatchSaveCoregistrationMatrix(matlabbatch, BIDS, opt, subLabel); - % dependency from file selector ('Anatomical') - matlabbatch = setBatchSegmentation(matlabbatch, opt); + % dependency from file selector ('Anatomical') + matlabbatch = setBatchSegmentation(matlabbatch, opt); - matlabbatch = setBatchSkullStripping(matlabbatch, BIDS, opt, subID); + matlabbatch = setBatchSkullStripping(matlabbatch, BIDS, opt, subLabel); - if strcmp(opt.space, 'MNI') - % dependency from segmentation - % dependency from coregistration - matlabbatch = setBatchNormalizationSpatialPrepro(matlabbatch, opt, voxDim); - end + if strcmp(opt.space, 'MNI') + % dependency from segmentation + % dependency from coregistration + matlabbatch = setBatchNormalizationSpatialPrepro(matlabbatch, opt, voxDim); + end - % if no unwarping was done on func, we reslice the func, so we can use - % them for the functionalQA - if ~opt.realign.useUnwarp - matlabbatch = setBatchRealign(matlabbatch, 'reslice', BIDS, opt, subID); - end + % if no unwarping was done on func, we reslice the func, so we can use + % them for the functionalQA + if ~opt.realign.useUnwarp + matlabbatch = setBatchRealign(matlabbatch, 'reslice', BIDS, opt, subLabel); + end - batchName = ['spatial_preprocessing-' upper(opt.space(1)) opt.space(2:end)]; + batchName = ['spatial_preprocessing-' upper(opt.space(1)) opt.space(2:end)]; - saveAndRunWorkflow(matlabbatch, batchName, opt, subID); + saveAndRunWorkflow(matlabbatch, batchName, opt, subLabel); - copyFigures(BIDS, opt, subID); + copyFigures(BIDS, opt, subLabel); - end end end diff --git a/src/workflows/saveAndRunWorkflow.m b/src/workflows/saveAndRunWorkflow.m index 363b305b..d6972712 100644 --- a/src/workflows/saveAndRunWorkflow.m +++ b/src/workflows/saveAndRunWorkflow.m @@ -1,6 +1,6 @@ % (C) Copyright 2019 CPP BIDS SPM-pipeline developers -function saveAndRunWorkflow(matlabbatch, batchName, opt, subID) +function saveAndRunWorkflow(matlabbatch, batchName, opt, subLabel) % % Saves the SPM matlabbatch and runs it % @@ -19,12 +19,12 @@ function saveAndRunWorkflow(matlabbatch, batchName, opt, subID) % :type subID: string if nargin < 4 - subID = []; + subLabel = []; end if ~isempty(matlabbatch) - saveMatlabBatch(matlabbatch, batchName, opt, subID); + saveMatlabBatch(matlabbatch, batchName, opt, subLabel); spm_jobman('run', matlabbatch); diff --git a/src/workflows/setUpWorkflow.m b/src/workflows/setUpWorkflow.m index 60e36f47..d79ffb29 100644 --- a/src/workflows/setUpWorkflow.m +++ b/src/workflows/setUpWorkflow.m @@ -1,6 +1,6 @@ % (C) Copyright 2019 CPP BIDS SPM-pipeline developers -function [BIDS, opt, group] = setUpWorkflow(opt, workflowName) +function [BIDS, opt] = setUpWorkflow(opt, workflowName) % % Calls some common functions to: % - check the configuraton, @@ -28,7 +28,7 @@ opt = loadAndCheckOptions(opt); % load the subjects/Groups information and the task name - [group, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); cleanCrash(); From 53a28a567dec351df841e2a02b1bf5c0aef06863 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Tue, 9 Mar 2021 10:31:15 +0100 Subject: [PATCH 010/145] rename subID to subLabel --- src/batches/setBatchCoregistrationFmap.m | 20 ++++++------ .../setBatchCoregistrationFuncToAnat.m | 10 +++--- src/batches/setBatchRealign.m | 20 +++++++----- src/batches/setBatchSTC.m | 8 ++--- .../setBatchSaveCoregistrationMatrix.m | 12 +++---- src/batches/setBatchSelectAnat.m | 8 ++--- src/batches/setBatchSkullStripping.m | 10 +++--- src/batches/setBatchSubjectLevelContrasts.m | 10 +++--- src/batches/setBatchSubjectLevelGLMSpec.m | 18 +++++------ src/batches/setBatchSubjectLevelResults.m | 10 +++--- src/getAnatFilename.m | 14 ++++----- src/getInfo.m | 16 +++++----- src/getMeanFuncFilename.m | 31 +++++++------------ src/reports/copyFigures.m | 16 +++++----- 14 files changed, 100 insertions(+), 103 deletions(-) diff --git a/src/batches/setBatchCoregistrationFmap.m b/src/batches/setBatchCoregistrationFmap.m index ca220f41..cdf5ab90 100644 --- a/src/batches/setBatchCoregistrationFmap.m +++ b/src/batches/setBatchCoregistrationFmap.m @@ -1,20 +1,20 @@ % (C) Copyright 2020 CPP BIDS SPM-pipeline developers -function matlabbatch = setBatchCoregistrationFmap(matlabbatch, BIDS, opt, subID) +function matlabbatch = setBatchCoregistrationFmap(matlabbatch, BIDS, opt, subLabel) % % Set the batch for the coregistration of field maps % % USAGE:: % - % matlabbatch = setBatchCoregistrationFmap(matlabbatch, BIDS, opt, subID) + % matlabbatch = setBatchCoregistrationFmap(matlabbatch, BIDS, opt, subLabel) % % :param BIDS: BIDS layout returned by ``getData``. % :type BIDS: structure % :param opt: structure or json filename containing the options. See % ``checkOptions()`` and ``loadAndCheckOptions()``. % :type opt: structure - % :param subID: subject ID - % :type subID: string + % :param subLabel: + % :type subLabel: string % % :returns: - :matlabbatch: (structure) The matlabbatch ready to run the spm job % @@ -26,23 +26,23 @@ % Use a rough mean of the 1rst run to improve SNR for coregistration % created by spmup - [sessions, nbSessions] = getInfo(BIDS, subID, opt, 'Sessions'); - runs = getInfo(BIDS, subID, opt, 'Runs', sessions{1}); - [fileName, subFuncDataDir] = getBoldFilename(BIDS, subID, sessions{1}, runs{1}, opt); + [sessions, nbSessions] = getInfo(BIDS, subLabel, opt, 'Sessions'); + runs = getInfo(BIDS, subLabel, opt, 'Runs', sessions{1}); + [fileName, subFuncDataDir] = getBoldFilename(BIDS, subLabel, sessions{1}, runs{1}, opt); refImage = validationInputFile(subFuncDataDir, fileName, 'mean_'); for iSes = 1:nbSessions runs = bids.query(BIDS, 'runs', ... 'modality', 'fmap', ... - 'sub', subID, ... + 'sub', subLabel, ... 'ses', sessions{iSes}); for iRun = 1:numel(runs) metadata = bids.query(BIDS, 'metadata', ... 'modality', 'fmap', ... - 'sub', subID, ... + 'sub', subLabel, ... 'ses', sessions{iSes}, ... 'run', runs{iRun}); @@ -50,7 +50,7 @@ fmapFiles = bids.query(BIDS, 'data', ... 'modality', 'fmap', ... - 'sub', subID, ... + 'sub', subLabel, ... 'ses', sessions{iSes}, ... 'run', runs{iRun}); diff --git a/src/batches/setBatchCoregistrationFuncToAnat.m b/src/batches/setBatchCoregistrationFuncToAnat.m index f1b54fe5..b9bc7dfa 100644 --- a/src/batches/setBatchCoregistrationFuncToAnat.m +++ b/src/batches/setBatchCoregistrationFuncToAnat.m @@ -1,6 +1,6 @@ % (C) Copyright 2020 CPP BIDS SPM-pipeline developers -function matlabbatch = setBatchCoregistrationFuncToAnat(matlabbatch, BIDS, opt, subID) +function matlabbatch = setBatchCoregistrationFuncToAnat(matlabbatch, BIDS, opt, subLabel) % % Set the batch for corregistering the functional images to the % anatomical image @@ -16,8 +16,8 @@ % :param opt: structure or json filename containing the options. See % ``checkOptions()`` and ``loadAndCheckOptions()``. % :type opt: structure - % :param subID: subject ID - % :type subID: string + % :param subLabel: subject ID + % :type subLabel: string % % :returns: - :matlabbatch: (structure) The matlabbatch ready to run the spm job % @@ -53,14 +53,14 @@ % OTHER IMAGES : DEPENDENCY FROM REALIGNEMENT - [sessions, nbSessions] = getInfo(BIDS, subID, opt, 'Sessions'); + [sessions, nbSessions] = getInfo(BIDS, subLabel, opt, 'Sessions'); runCounter = 1; for iSes = 1:nbSessions % get all runs for that subject for this session - [~, nbRuns] = getInfo(BIDS, subID, opt, 'Runs', sessions{iSes}); + [~, nbRuns] = getInfo(BIDS, subLabel, opt, 'Runs', sessions{iSes}); for iRun = 1:nbRuns diff --git a/src/batches/setBatchRealign.m b/src/batches/setBatchRealign.m index e509008e..a911aa12 100644 --- a/src/batches/setBatchRealign.m +++ b/src/batches/setBatchRealign.m @@ -6,7 +6,11 @@ % % USAGE:: % - % [matlabbatch, voxDim] = setBatchRealign(matlabbatch, [action = 'realign'], BIDS, opt, subID) + % [matlabbatch, voxDim] = setBatchRealign(matlabbatch, ... + % [action = 'realign'], ... + % BIDS, ... + % opt, ... + % subLabel) % % :param matlabbatch: SPM batch % :type matlabbatch: structure @@ -16,8 +20,8 @@ % :type action: string % :param opt: Options chosen for the analysis. See ``checkOptions()``. % :type opt: structure - % :type subID: string - % :param subID: subject label + % :type subLabel: string + % :param subLabel: subject label % % :returns: - :matlabbatch: (structure) (dimension) % - :voxDim: (array) (dimension) @@ -26,10 +30,10 @@ % make which image is resliced more consistent 'which = []' if numel(varargin) < 5 - [matlabbatch, BIDS, opt, subID] = deal(varargin{:}); + [matlabbatch, BIDS, opt, subLabel] = deal(varargin{:}); action = ''; else - [matlabbatch, action, BIDS, opt, subID] = deal(varargin{:}); + [matlabbatch, action, BIDS, opt, subLabel] = deal(varargin{:}); end if isempty(action) @@ -61,21 +65,21 @@ printBatchName(msg); - [sessions, nbSessions] = getInfo(BIDS, subID, opt, 'Sessions'); + [sessions, nbSessions] = getInfo(BIDS, subLabel, opt, 'Sessions'); runCounter = 1; for iSes = 1:nbSessions % get all runs for that subject across all sessions - [runs, nbRuns] = getInfo(BIDS, subID, opt, 'Runs', sessions{iSes}); + [runs, nbRuns] = getInfo(BIDS, subLabel, opt, 'Runs', sessions{iSes}); for iRun = 1:nbRuns % get the filename for this bold run for this task [boldFilename, subFuncDataDir] = getBoldFilename( ... BIDS, ... - subID, ... + subLabel, ... sessions{iSes}, ... runs{iRun}, ... opt); diff --git a/src/batches/setBatchSTC.m b/src/batches/setBatchSTC.m index 473687dd..9c44dca5 100644 --- a/src/batches/setBatchSTC.m +++ b/src/batches/setBatchSTC.m @@ -1,6 +1,6 @@ % (C) Copyright 2019 CPP BIDS SPM-pipeline developers -function matlabbatch = setBatchSTC(matlabbatch, BIDS, opt, subID) +function matlabbatch = setBatchSTC(matlabbatch, BIDS, opt, subLabel) % % Creates batch for slice timing correction % @@ -83,21 +83,21 @@ matlabbatch{end}.spm.temporal.st.so = sliceOrder * 1000; matlabbatch{end}.spm.temporal.st.refslice = referenceSlice * 1000; - [sessions, nbSessions] = getInfo(BIDS, subID, opt, 'Sessions'); + [sessions, nbSessions] = getInfo(BIDS, subLabel, opt, 'Sessions'); runCounter = 1; for iSes = 1:nbSessions % get all runs for that subject for this session - [runs, nbRuns] = getInfo(BIDS, subID, opt, 'Runs', sessions{iSes}); + [runs, nbRuns] = getInfo(BIDS, subLabel, opt, 'Runs', sessions{iSes}); for iRun = 1:nbRuns % get the filename for this bold run for this task [fileName, subFuncDataDir] = getBoldFilename( ... BIDS, ... - subID, sessions{iSes}, runs{iRun}, opt); + subLabel, sessions{iSes}, runs{iRun}, opt); % check that the file with the right prefix exist file = validationInputFile(subFuncDataDir, fileName); diff --git a/src/batches/setBatchSaveCoregistrationMatrix.m b/src/batches/setBatchSaveCoregistrationMatrix.m index 82212043..ab9fb872 100644 --- a/src/batches/setBatchSaveCoregistrationMatrix.m +++ b/src/batches/setBatchSaveCoregistrationMatrix.m @@ -1,6 +1,6 @@ % (C) Copyright 2020 CPP BIDS SPM-pipeline developers -function matlabbatch = setBatchSaveCoregistrationMatrix(matlabbatch, BIDS, opt, subID) +function matlabbatch = setBatchSaveCoregistrationMatrix(matlabbatch, BIDS, opt, subLabel) % % Short description of what the function does goes here. % @@ -14,8 +14,8 @@ % :type BIDS: structure % :param opt: % :type opt: Options chosen for the analysis. See ``checkOptions()``. - % :param subID: - % :type subID: + % :param subLabel: + % :type subLabel: string % % :returns: - :matlabbatch: % @@ -24,11 +24,11 @@ % create name of the output file based on the name of the first image of the % first session - sessions = getInfo(BIDS, subID, opt, 'Sessions'); - runs = getInfo(BIDS, subID, opt, 'Runs', sessions{1}); + sessions = getInfo(BIDS, subLabel, opt, 'Sessions'); + runs = getInfo(BIDS, subLabel, opt, 'Runs', sessions{1}); [fileName, subFuncDataDir] = getBoldFilename( ... BIDS, ... - subID, sessions{1}, runs{1}, opt); + subLabel, sessions{1}, runs{1}, opt); fileName = strrep(fileName, '_bold.nii', '_from-scanner_to-T1w_mode-image_xfm.mat'); diff --git a/src/batches/setBatchSelectAnat.m b/src/batches/setBatchSelectAnat.m index cb60668f..e8e11bd1 100644 --- a/src/batches/setBatchSelectAnat.m +++ b/src/batches/setBatchSelectAnat.m @@ -1,6 +1,6 @@ % (C) Copyright 2020 CPP BIDS SPM-pipeline developers -function matlabbatch = setBatchSelectAnat(matlabbatch, BIDS, opt, subID) +function matlabbatch = setBatchSelectAnat(matlabbatch, BIDS, opt, subLabel) % % Creates a batch to set an anatomical image % @@ -15,8 +15,8 @@ % :param opt: structure or json filename containing the options. See % ``checkOptions()`` and ``loadAndCheckOptions()``. % :type opt: structure - % :param subID: subject ID - % :type subID: string + % :param subLabel: subject label + % :type subLabel: string % % :returns: :matlabbatch: (structure) % @@ -29,7 +29,7 @@ printBatchName('selecting anatomical image'); - [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); + [anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt); matlabbatch{end + 1}.cfg_basicio.cfg_named_file.name = 'Anatomical'; matlabbatch{end}.cfg_basicio.cfg_named_file.files = { {fullfile(anatDataDir, anatImage)} }; diff --git a/src/batches/setBatchSkullStripping.m b/src/batches/setBatchSkullStripping.m index 286b65a6..52166108 100644 --- a/src/batches/setBatchSkullStripping.m +++ b/src/batches/setBatchSkullStripping.m @@ -1,13 +1,13 @@ % (C) Copyright 2020 CPP BIDS SPM-pipeline developers -function matlabbatch = setBatchSkullStripping(matlabbatch, BIDS, opt, subID) +function matlabbatch = setBatchSkullStripping(matlabbatch, BIDS, opt, subLabel) % % Creates a batch to compute a brain mask based on the tissue probability maps % from the segmentation. % % USAGE:: % - % matlabbatch = setBatchSkullStripping(matlabbatch, BIDS, opt, subID) + % matlabbatch = setBatchSkullStripping(matlabbatch, BIDS, opt, subLabel) % % :param matlabbatch: list of SPM batches % :type matlabbatch: structure @@ -16,8 +16,8 @@ % :param opt: structure or json filename containing the options. See % ``checkOptions()`` and ``loadAndCheckOptions()``. % :type opt: structure - % :param subID: subject ID - % :type subID: string + % :param subLabel: subject ID + % :type subLabel: string % % :returns: - :matlabbatch: (structure) The matlabbatch ready to run the spm job % @@ -36,7 +36,7 @@ printBatchName('skull stripping'); - [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); + [anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt); output = ['m' strrep(anatImage, '.nii', '_skullstripped.nii')]; expression = sprintf('i1.*((i2+i3+i4)>%f)', opt.skullstrip.threshold); diff --git a/src/batches/setBatchSubjectLevelContrasts.m b/src/batches/setBatchSubjectLevelContrasts.m index ad971576..593d4973 100644 --- a/src/batches/setBatchSubjectLevelContrasts.m +++ b/src/batches/setBatchSubjectLevelContrasts.m @@ -1,19 +1,19 @@ % (C) Copyright 2019 CPP BIDS SPM-pipeline developers -function matlabbatch = setBatchSubjectLevelContrasts(matlabbatch, opt, subID, funcFWHM) +function matlabbatch = setBatchSubjectLevelContrasts(matlabbatch, opt, subLabel, funcFWHM) % % Short description of what the function does goes here. % % USAGE:: % - % matlabbatch = setBatchSubjectLevelContrasts(matlabbatch, opt, subID, funcFWHM) + % matlabbatch = setBatchSubjectLevelContrasts(matlabbatch, opt, subLabel, funcFWHM) % % :param matlabbatch: % :type matlabbatch: structure % :param opt: % :type opt: structure - % :param subID: - % :type subID: string + % :param subLabel: + % :type subLabel: string % :param funcFWHM: % :type funcFWHM: % @@ -22,7 +22,7 @@ printBatchName('subject level contrasts specification'); - ffxDir = getFFXdir(subID, funcFWHM, opt); + ffxDir = getFFXdir(subLabel, funcFWHM, opt); spmMatFile = cellstr(fullfile(ffxDir, 'SPM.mat')); diff --git a/src/batches/setBatchSubjectLevelGLMSpec.m b/src/batches/setBatchSubjectLevelGLMSpec.m index 2b353d61..65688901 100644 --- a/src/batches/setBatchSubjectLevelGLMSpec.m +++ b/src/batches/setBatchSubjectLevelGLMSpec.m @@ -6,7 +6,7 @@ % % USAGE:: % - % matlabbatch = setBatchSubjectLevelGLMSpec(matlabbatch, BIDS, opt, subID, funcFWHM) + % matlabbatch = setBatchSubjectLevelGLMSpec(matlabbatch, BIDS, opt, subLabel, funcFWHM) % % :param argin1: (dimension) obligatory argument. Lorem ipsum dolor sit amet, % consectetur adipiscing elit. Ut congue nec est ac lacinia. @@ -19,7 +19,7 @@ % - :argout2: (type) (dimension) % - [matlabbatch, BIDS, opt, subID, funcFWHM] = deal(varargin{:}); + [matlabbatch, BIDS, opt, subLabel, funcFWHM] = deal(varargin{:}); printBatchName('specify subject level fmri model'); @@ -34,7 +34,7 @@ % slice in the first bold image to set the number of time bins % we will use to upsample our model during regression creation fileName = bids.query(BIDS, 'data', ... - 'sub', subID, ... + 'sub', subLabel, ... 'type', 'bold'); fileName = strrep(fileName{1}, '.gz', ''); hdr = spm_vol(fileName); @@ -64,7 +64,7 @@ % Create ffxDir if it doesnt exist % If it exists, issue a warning that it has been overwritten - ffxDir = getFFXdir(subID, funcFWHM, opt); + ffxDir = getFFXdir(subLabel, funcFWHM, opt); if exist(ffxDir, 'dir') % warning('overwriting directory: %s \n', ffxDir); rmdir(ffxDir, 's'); @@ -87,20 +87,20 @@ % matlabbatch{end}.spm.stats.fmri_spec.cvi = 'AR(1)'; % identify sessions for this subject - [sessions, nbSessions] = getInfo(BIDS, subID, opt, 'Sessions'); + [sessions, nbSessions] = getInfo(BIDS, subLabel, opt, 'Sessions'); sesCounter = 1; for iSes = 1:nbSessions % get all runs for that subject across all sessions [runs, nbRuns] = ... - getInfo(BIDS, subID, opt, 'Runs', sessions{iSes}); + getInfo(BIDS, subLabel, opt, 'Runs', sessions{iSes}); for iRun = 1:nbRuns % get functional files [fullpathBoldFileName, prefix] = ... - getBoldFilenameForFFX(BIDS, opt, subID, funcFWHM, iSes, iRun); + getBoldFilenameForFFX(BIDS, opt, subLabel, funcFWHM, iSes, iRun); disp(fullpathBoldFileName); @@ -108,12 +108,12 @@ {fullpathBoldFileName}; % get stimuli onset time file - tsvFile = getInfo(BIDS, subID, opt, 'filename', ... + tsvFile = getInfo(BIDS, subLabel, opt, 'filename', ... sessions{iSes}, ... runs{iRun}, ... 'events'); fullpathOnsetFileName = createAndReturnOnsetFile(opt, ... - subID, ... + subLabel, ... tsvFile, ... funcFWHM); diff --git a/src/batches/setBatchSubjectLevelResults.m b/src/batches/setBatchSubjectLevelResults.m index 0b5cd0f9..1f8aa354 100644 --- a/src/batches/setBatchSubjectLevelResults.m +++ b/src/batches/setBatchSubjectLevelResults.m @@ -12,8 +12,8 @@ % :type matlabbatch: structure % :param opt: % :type opt: structure - % :param subID: - % :type subID: string + % :param subLabel: + % :type subLabel: string % :param funcFWHM: % :type funcFWHM: float % :param iStep: @@ -24,7 +24,7 @@ % :returns: - :matlabbatch: (structure) % - [matlabbatch, opt, subID, funcFWHM, iStep, iCon] = deal(varargin{:}); + [matlabbatch, opt, subLabel, funcFWHM, iStep, iCon] = deal(varargin{:}); result.Contrasts = opt.result.Steps(iStep).Contrasts(iCon); @@ -33,8 +33,8 @@ end result.space = opt.space; - result.dir = getFFXdir(subID, funcFWHM, opt); - result.label = subID; + result.dir = getFFXdir(subLabel, funcFWHM, opt); + result.label = subLabel; result.nbSubj = 1; result.contrastNb = getContrastNb(result); diff --git a/src/getAnatFilename.m b/src/getAnatFilename.m index 74b83485..03a21add 100644 --- a/src/getAnatFilename.m +++ b/src/getAnatFilename.m @@ -1,6 +1,6 @@ % (C) Copyright 2020 CPP BIDS SPM-pipeline developers -function [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt) +function [anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt) % % Short description of what the function does goes here. % @@ -18,7 +18,7 @@ % :returns: - :argout1: (type) (dimension) % - :argout2: (type) (dimension) % - % [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt) + % [anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt) % % Get the filename and the directory of an anat file for a given session / % run. @@ -26,27 +26,27 @@ anatType = opt.anatReference.type; - sessions = getInfo(BIDS, subID, opt, 'Sessions'); + sessions = getInfo(BIDS, subLabel, opt, 'Sessions'); % get all anat images for that subject fo that type % TODO allow for the session to be referenced by a string e.g ses-retest anat = bids.query(BIDS, 'data', ... - 'sub', subID, ... + 'sub', subLabel, ... 'type', anatType); if ~isempty(opt.anatReference.session) anatSession = opt.anatReference.session; anat = bids.query(BIDS, 'data', ... - 'sub', subID, ... + 'sub', subLabel, ... 'ses', sessions{anatSession}, ... 'type', anatType); end if isempty(anat) anat = bids.query(BIDS, 'data', ... - 'sub', subID, ... + 'sub', subLabel, ... 'type', anatType); error('No anat file for the subject %s. Here are all anat file:\n%s', ... - subID, ... + subLabel, ... char(anat)); end diff --git a/src/getInfo.m b/src/getInfo.m index 8e56ef5a..01bbe415 100644 --- a/src/getInfo.m +++ b/src/getInfo.m @@ -1,22 +1,22 @@ % (C) Copyright 2020 CPP BIDS SPM-pipeline developers -function varargout = getInfo(BIDS, subID, opt, info, varargin) +function varargout = getInfo(BIDS, subLabel, opt, info, varargin) % % Wrapper function to fetch specific info in a BIDS structure returned by % spm_bids. % % USAGE:: % - % varargout = getInfo(BIDS, subID, opt, info, varargin) + % varargout = getInfo(BIDS, subLabel, opt, info, varargin) % % :param BIDS: (structure) returned by bids.query when exploring a BIDS data set. - % :param subID: ID of the subject + % :param subLabel: ID of the subject % :param opt: (structure) Mostly used to find the task name. % :param info: (strint) ``sessions``, ``runs``, ``filename``. % :param varargin: see below % - % - subID - ID of the subject ; in BIDS lingo that means that for a file name - % ``sub-02_task-foo_bold.nii`` the subID will be the string ``02`` + % - subLabel - ID of the subject ; in BIDS lingo that means that for a file name + % ``sub-02_task-foo_bold.nii`` the subLabel will be the string ``02`` % - session - ID of the session of interes ; in BIDS lingo that means that for a file name % ``sub-02_ses-pretest_task-foo_bold.nii`` the sesssion will be the string % ``pretest`` @@ -41,7 +41,7 @@ case 'sessions' sessions = bids.query(BIDS, 'sessions', ... - 'sub', subID, ... + 'sub', subLabel, ... 'task', opt.taskName); nbSessions = size(sessions, 2); if nbSessions == 0 @@ -56,7 +56,7 @@ session = varargin{1}; runs = bids.query(BIDS, 'runs', ... - 'sub', subID, ... + 'sub', subLabel, ... 'task', opt.taskName, ... 'ses', session, ... 'type', 'bold'); @@ -74,7 +74,7 @@ [session, run, type] = deal(varargin{:}); varargout = bids.query(BIDS, 'data', ... - 'sub', subID, ... + 'sub', subLabel, ... 'run', run, ... 'ses', session, ... 'task', opt.taskName, ... diff --git a/src/getMeanFuncFilename.m b/src/getMeanFuncFilename.m index 98297502..b9057cc6 100644 --- a/src/getMeanFuncFilename.m +++ b/src/getMeanFuncFilename.m @@ -1,36 +1,29 @@ % (C) Copyright 2020 CPP BIDS SPM-pipeline developers -function [meanImage, meanFuncDir] = getMeanFuncFilename(BIDS, subID, opt) +function [meanImage, meanFuncDir] = getMeanFuncFilename(BIDS, subLabel, opt) % - % Short description of what the function does goes here. + % Get the filename and the directory of an mean functional file. % % USAGE:: % - % [argout1, argout2] = templateFunction(argin1, [argin2 == default,] [argin3]) + % [meanImage, meanFuncDir] = getMeanFuncFilename(BIDS, subLabel, opt) % - % :param argin1: (dimension) obligatory argument. Lorem ipsum dolor sit amet, - % consectetur adipiscing elit. Ut congue nec est ac lacinia. - % :type argin1: type - % :param argin2: optional argument and its default value. And some of the - % options can be shown in litteral like ``this`` or ``that``. - % :type argin2: string + % :param BIDS: + % :type BIDS: structure + % :param subLabel: + % :type subLabel: string % :param opt: Options chosen for the analysis. See ``checkOptions()``. % :type opt: structure % - % :returns: - :argout1: (type) (dimension) - % - :argout2: (type) (dimension) + % :returns: - :meanImage: (string) + % - :meanFuncDir: (string) % - % [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt) - % - % Get the filename and the directory of an anat file for a given session / - % run. - % Unzips the file if necessary. - sessions = getInfo(BIDS, subID, opt, 'Sessions'); - runs = getInfo(BIDS, subID, opt, 'Runs', sessions{1}); + sessions = getInfo(BIDS, subLabel, opt, 'Sessions'); + runs = getInfo(BIDS, subLabel, opt, 'Runs', sessions{1}); [boldFileName, subFuncDataDir] = getBoldFilename( ... BIDS, ... - subID, sessions{1}, runs{1}, opt); + subLabel, sessions{1}, runs{1}, opt); prefix = getPrefix('mean', opt); diff --git a/src/reports/copyFigures.m b/src/reports/copyFigures.m index 607aecd1..04bcce9f 100644 --- a/src/reports/copyFigures.m +++ b/src/reports/copyFigures.m @@ -1,29 +1,29 @@ % (C) Copyright 2019 CPP BIDS SPM-pipeline developers -function copyFigures(BIDS, opt, subID) +function copyFigures(BIDS, opt, subLabel) % % Copy the figures from spatial preprocessing into a separate folder. % % USAGE:: % - % copyFigures(BIDS, opt, subID) + % copyFigures(BIDS, opt, subLabel) % % :param BIDS: BIDS layout returned by ``getData``. % :type BIDS: structure % :param opt: Options chosen for the analysis. See ``checkOptions()``. % :type opt: structure - % :param subID: Subject label (for example `'01'`). - % :type subID: string + % :param subLabel: Subject label (for example `'01'`). + % :type subLabel: string % % - imgNb = copyGraphWindownOutput(opt, subID, 'realign'); + imgNb = copyGraphWindownOutput(opt, subLabel, 'realign'); % loop through the figures outputed for unwarp: one per run if opt.realign.useUnwarp runs = bids.query(BIDS, 'runs', ... - 'sub', subID, ... + 'sub', subLabel, ... 'task', opt.taskName, ... 'type', 'bold'); @@ -32,10 +32,10 @@ function copyFigures(BIDS, opt, subID) nbRuns = 1; end - imgNb = copyGraphWindownOutput(opt, subID, 'unwarp', imgNb:(imgNb + nbRuns - 1)); + imgNb = copyGraphWindownOutput(opt, subLabel, 'unwarp', imgNb:(imgNb + nbRuns - 1)); end - imgNb = copyGraphWindownOutput(opt, subID, 'func2anatCoreg', imgNb); %#ok + imgNb = copyGraphWindownOutput(opt, subLabel, 'func2anatCoreg', imgNb); %#ok end From babc809df695d5cc8054d58d289cf38b9a240902 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Tue, 9 Mar 2021 10:32:08 +0100 Subject: [PATCH 011/145] update and refactor tests and lint --- src/batches/setBatchMeanAnatAndMask.m | 28 +++++------ tests/test_bidsCopyRawFolder.m | 6 +-- tests/test_createAndReturnOnsetFile.m | 17 ++++--- tests/test_createDataDictionary.m | 17 ++++--- tests/test_createDefaultModel.m | 5 +- tests/test_getAnatFilename.m | 11 ++--- tests/test_getBoldFilename.m | 17 ++++--- tests/test_getBoldFilenameForFFX.m | 20 ++++---- tests/test_getData.m | 21 +++------ tests/test_getFFXdir.m | 18 +++----- tests/test_getInfo.m | 11 ++--- tests/test_getMeanFuncFilename.m | 12 ++--- tests/test_getPrefix.m | 2 + tests/test_getRFXdir.m | 4 +- tests/test_getRealignParamFile.m | 34 +++++--------- tests/test_getSliceOrder.m | 15 ++---- tests/test_getSubjectList.m | 27 ++++------- tests/test_setBatchCoregistrationFuncToAnat.m | 23 ++++------ tests/test_setBatchFactorialDesign.m | 3 +- tests/test_setBatchMeanAnatAndMask.m | 8 +--- tests/test_setBatchRealign.m | 14 ++---- tests/test_setBatchSTC.m | 46 +++++++++---------- tests/test_setBatchSaveCoregistrationMatrix.m | 10 ++-- tests/test_setBatchSegmentation.m | 8 ++-- tests/test_setBatchSelectAnat.m | 11 ++--- tests/test_setBatchSkullStripping.m | 12 ++--- tests/test_setBatchSmoothConImages.m | 16 +++---- tests/test_setBatchSmoothingFunc.m | 13 ++---- tests/test_setBatchSubjectLevelContrasts.m | 8 ++-- tests/test_setBatchSubjectLevelGLMSpec.m | 20 +++----- tests/test_setBatchSubjectLevelResults.m | 31 ++++++------- tests/test_specifyContrasts.m | 13 +++--- tests/utils/setOptions.m | 22 +++++++++ 33 files changed, 220 insertions(+), 303 deletions(-) create mode 100644 tests/utils/setOptions.m diff --git a/src/batches/setBatchMeanAnatAndMask.m b/src/batches/setBatchMeanAnatAndMask.m index 99240062..41839db5 100644 --- a/src/batches/setBatchMeanAnatAndMask.m +++ b/src/batches/setBatchMeanAnatAndMask.m @@ -28,27 +28,27 @@ for iSub = 1:numel(opt.subjects) - subLabel = opt.subjects{iSub}; + subLabel = opt.subjects{iSub}; - printProcessingSubject(iSub, subLabel); + printProcessingSubject(iSub, subLabel); - %% Anat - [anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt); + %% Anat + [anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt); - anatImage = validationInputFile( ... - anatDataDir, ... - anatImage, ... - [spm_get_defaults('normalise.write.prefix'), ... - spm_get_defaults('deformations.modulate.prefix')]); + anatImage = validationInputFile( ... + anatDataDir, ... + anatImage, ... + [spm_get_defaults('normalise.write.prefix'), ... + spm_get_defaults('deformations.modulate.prefix')]); - inputAnat{end + 1, 1} = anatImage; %#ok<*AGROW> + inputAnat{end + 1, 1} = anatImage; %#ok<*AGROW> - %% Mask - ffxDir = getFFXdir(subLabel, funcFWHM, opt); + %% Mask + ffxDir = getFFXdir(subLabel, funcFWHM, opt); - files = validationInputFile(ffxDir, 'mask.nii'); + files = validationInputFile(ffxDir, 'mask.nii'); - inputMask{end + 1, 1} = files; + inputMask{end + 1, 1} = files; end diff --git a/tests/test_bidsCopyRawFolder.m b/tests/test_bidsCopyRawFolder.m index ad8adadc..d6e7d215 100644 --- a/tests/test_bidsCopyRawFolder.m +++ b/tests/test_bidsCopyRawFolder.m @@ -8,11 +8,7 @@ function test_bidsCopyRawFolderBasic() - opt.dataDir = fullfile( ... - fileparts(mfilename('fullpath')), ... - '..', 'demos', 'MoAE', 'output', 'MoAEpilot'); - - opt.taskName = 'auditory'; + opt = setOptions('MoAE'); opt = checkOptions(opt); diff --git a/tests/test_createAndReturnOnsetFile.m b/tests/test_createAndReturnOnsetFile.m index a3189511..492dafc8 100644 --- a/tests/test_createAndReturnOnsetFile.m +++ b/tests/test_createAndReturnOnsetFile.m @@ -8,28 +8,27 @@ function test_createAndReturnOnsetFileBasic() - subID = '01'; + subLabel = '01'; funcFWHM = 6; iSes = 1; iRun = 1; - opt.taskName = 'vislocalizer'; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.subjects = {'01'}; + opt = setOptions('vislocalizer', subLabel); + opt.model.file = fullfile(fileparts(mfilename('fullpath')), ... 'dummyData', 'models', ... 'model-vislocalizer_smdl.json'); opt = checkOptions(opt); - [~, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); - sessions = getInfo(BIDS, subID, opt, 'sessions'); - runs = getInfo(BIDS, subID, opt, 'runs', sessions{iSes}); + sessions = getInfo(BIDS, subLabel, opt, 'sessions'); + runs = getInfo(BIDS, subLabel, opt, 'runs', sessions{iSes}); - tsvFile = getInfo(BIDS, subID, opt, 'filename', sessions{iSes}, runs{iRun}, 'events'); + tsvFile = getInfo(BIDS, subLabel, opt, 'filename', sessions{iSes}, runs{iRun}, 'events'); - onsetFileName = createAndReturnOnsetFile(opt, subID, tsvFile, funcFWHM); + onsetFileName = createAndReturnOnsetFile(opt, subLabel, tsvFile, funcFWHM); expectedFileName = fullfile(fileparts(mfilename('fullpath')), ... 'dummyData', 'derivatives', 'cpp_spm', 'sub-01', 'stats', ... diff --git a/tests/test_createDataDictionary.m b/tests/test_createDataDictionary.m index 1ee7ceb5..f33d63c1 100644 --- a/tests/test_createDataDictionary.m +++ b/tests/test_createDataDictionary.m @@ -8,24 +8,23 @@ function test_createDataDictionaryBasic() - subID = '01'; + subLabel = '01'; iSes = 1; iRun = 1; - opt.taskName = 'vislocalizer'; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.groups = {''}; - opt.subjects = {'01'}; + opt = setOptions('vislocalizer', subLabel); - [~, opt, BIDS] = getData(opt); + opt = checkOptions(opt); - sessions = getInfo(BIDS, subID, opt, 'Sessions'); + [BIDS, opt] = getData(opt); - runs = getInfo(BIDS, subID, opt, 'Runs', sessions{iSes}); + sessions = getInfo(BIDS, subLabel, opt, 'Sessions'); + + runs = getInfo(BIDS, subLabel, opt, 'Runs', sessions{iSes}); [fileName, subFuncDataDir] = getBoldFilename( ... BIDS, ... - subID, sessions{iSes}, runs{iRun}, opt); + subLabel, sessions{iSes}, runs{iRun}, opt); createDataDictionary(subFuncDataDir, fileName, 3); diff --git a/tests/test_createDefaultModel.m b/tests/test_createDefaultModel.m index b10f963b..77d878c1 100644 --- a/tests/test_createDefaultModel.m +++ b/tests/test_createDefaultModel.m @@ -8,12 +8,11 @@ function test_createDefaultModelBasic() - opt.taskName = 'vislocalizer'; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); + opt = setOptions('vislocalizer'); opt = checkOptions(opt); - [~, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); createDefaultModel(BIDS, opt); diff --git a/tests/test_getAnatFilename.m b/tests/test_getAnatFilename.m index c888f724..92b73333 100644 --- a/tests/test_getAnatFilename.m +++ b/tests/test_getAnatFilename.m @@ -14,18 +14,15 @@ function test_getAnatFilenameBasic() - subID = '01'; + subLabel = '01'; - opt.taskName = 'vislocalizer'; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.groups = {''}; - opt.subjects = {subID}; + opt = setOptions('vislocalizer', subLabel); opt = checkOptions(opt); - [~, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); - [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); + [anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt); expectedFileName = 'sub-01_ses-01_T1w.nii'; diff --git a/tests/test_getBoldFilename.m b/tests/test_getBoldFilename.m index be89963e..bd175ffc 100644 --- a/tests/test_getBoldFilename.m +++ b/tests/test_getBoldFilename.m @@ -8,25 +8,24 @@ function test_getBoldFilenameBasic() - subID = '01'; + subLabel = '01'; funcFWHM = 6; iSes = 1; iRun = 1; - opt.taskName = 'vislocalizer'; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.groups = {''}; - opt.subjects = {'01'}; + opt = setOptions('vislocalizer', subLabel); - [~, opt, BIDS] = getData(opt); + opt = checkOptions(opt); - sessions = getInfo(BIDS, subID, opt, 'Sessions'); + [BIDS, opt] = getData(opt); - runs = getInfo(BIDS, subID, opt, 'Runs', sessions{iSes}); + sessions = getInfo(BIDS, subLabel, opt, 'Sessions'); + + runs = getInfo(BIDS, subLabel, opt, 'Runs', sessions{iSes}); [fileName, subFuncDataDir] = getBoldFilename( ... BIDS, ... - subID, sessions{iSes}, runs{iRun}, opt); + subLabel, sessions{iSes}, runs{iRun}, opt); expectedFileName = 'sub-01_ses-01_task-vislocalizer_bold.nii'; diff --git a/tests/test_getBoldFilenameForFFX.m b/tests/test_getBoldFilenameForFFX.m index bfbc0c25..2ca8f48e 100644 --- a/tests/test_getBoldFilenameForFFX.m +++ b/tests/test_getBoldFilenameForFFX.m @@ -8,20 +8,18 @@ function test_getBoldFilenameForFFXBasic() - subID = '01'; + subLabel = '01'; funcFWHM = 6; iSes = 1; iRun = 1; - opt.taskName = 'vislocalizer'; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.subjects = {'01'}; + opt = setOptions('vislocalizer', subLabel); opt = checkOptions(opt); - [~, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); - [boldFileName, prefix] = getBoldFilenameForFFX(BIDS, opt, subID, funcFWHM, iSes, iRun); + [boldFileName, prefix] = getBoldFilenameForFFX(BIDS, opt, subLabel, funcFWHM, iSes, iRun); expectedFileName = fullfile(fileparts(mfilename('fullpath')), ... 'dummyData', 'derivatives', 'cpp_spm', 'sub-01', ... @@ -35,21 +33,19 @@ function test_getBoldFilenameForFFXBasic() function test_getBoldFilenameForFFXNativeSpace() - subID = '01'; + subLabel = '01'; funcFWHM = 6; iSes = 1; iRun = 1; - opt.taskName = 'vislocalizer'; - opt.dataDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData', 'derivatives'); - opt.subjects = {'01'}; + opt = setOptions('vislocalizer', subLabel); opt.space = 'individual'; opt = checkOptions(opt); - [~, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); - [boldFileName, prefix] = getBoldFilenameForFFX(BIDS, opt, subID, funcFWHM, iSes, iRun); + [boldFileName, prefix] = getBoldFilenameForFFX(BIDS, opt, subLabel, funcFWHM, iSes, iRun); expectedFileName = fullfile(fileparts(mfilename('fullpath')), ... 'dummyData', 'derivatives', 'cpp_spm', 'sub-01', ... diff --git a/tests/test_getData.m b/tests/test_getData.m index 105718c9..64a134d5 100644 --- a/tests/test_getData.m +++ b/tests/test_getData.m @@ -6,15 +6,12 @@ initTestSuite; end -function test_getDataBasic() +function test_getDataMetadata() - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'vismotion'; + subLabel = '01'; - %% Only get anat metadata - opt.groups = {''}; - - opt.subjects = {'01'}; + opt = setOptions('vismotion', subLabel); + opt = checkOptions(opt); [~, opt] = getData(opt, [], 'T1w'); @@ -23,15 +20,9 @@ function test_getDataBasic() end function test_getDataErrorTask() - % Small test to ensure that getData returns what we asked for - - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'testTask'; - opt.zeropad = 2; - %% Get all groups all subjects - opt.groups = {''}; - opt.subjects = {[]}; + opt = setOptions('testTask'); + opt = checkOptions(opt); assertExceptionThrown( ... @()getData(opt), ... diff --git a/tests/test_getFFXdir.m b/tests/test_getFFXdir.m index ba269464..0fddb075 100644 --- a/tests/test_getFFXdir.m +++ b/tests/test_getFFXdir.m @@ -9,19 +9,17 @@ function test_getFFXdirBasic() funcFWFM = 0; - subID = '01'; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'funcLocalizer'; + subLabel = '01'; + opt = setOptions('vislocalizer', subLabel); opt = setDerivativesDir(opt); - opt = checkOptions(opt); expectedOutput = fullfile(fileparts(mfilename('fullpath')), 'dummyData', 'derivatives', ... 'cpp_spm', 'sub-01', 'stats', 'ffx_task-funcLocalizer', ... 'ffx_space-MNI_FWHM-0'); - ffxDir = getFFXdir(subID, funcFWFM, opt); + ffxDir = getFFXdir(subLabel, funcFWFM, opt); assertEqual(exist(expectedOutput, 'dir'), 7); @@ -30,20 +28,18 @@ function test_getFFXdirBasic() function test_getFFXdirMvpa() funcFWFM = 6; - subID = '02'; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'nBack'; - opt.space = 'individual'; + subLabel = '02'; + opt = setOptions('nBack', subLabel); + opt.space = 'individual'; opt = setDerivativesDir(opt); - opt = checkOptions(opt); expectedOutput = fullfile(fileparts(mfilename('fullpath')), 'dummyData', 'derivatives', ... 'cpp_spm', 'sub-02', 'stats', 'ffx_task-nBack', ... 'ffx_space-individual_FWHM-6'); - ffxDir = getFFXdir(subID, funcFWFM, opt); + ffxDir = getFFXdir(subLabel, funcFWFM, opt); assertEqual(exist(expectedOutput, 'dir'), 7); diff --git a/tests/test_getInfo.m b/tests/test_getInfo.m index 837a276e..fe4a973c 100644 --- a/tests/test_getInfo.m +++ b/tests/test_getInfo.m @@ -13,14 +13,13 @@ function test_getInfoBasic() opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), ... 'dummyData', 'derivatives', 'cpp_spm'); - opt.groups = {''}; - opt.subjects = {[], []}; + opt = checkOptions(opt); %% Get sessions from BIDS opt.taskName = 'vismotion'; subID = 'ctrl01'; info = 'sessions'; - [~, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); sessions = getInfo(BIDS, subID, opt, info); assert(all(strcmp(sessions, {'01' '02'}))); @@ -29,7 +28,7 @@ function test_getInfoBasic() subID = 'ctrl01'; info = 'runs'; session = '01'; - [~, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); runs = getInfo(BIDS, subID, opt, info, session); assert(all(strcmp(runs, {'1' '2'}))); @@ -38,7 +37,7 @@ function test_getInfoBasic() subID = 'ctrl01'; info = 'runs'; session = '01'; - [~, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); runs = getInfo(BIDS, subID, opt, info, session); assert(strcmp(runs, {''})); @@ -48,7 +47,7 @@ function test_getInfoBasic() session = '01'; run = '1'; info = 'filename'; - [~, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); filename = getInfo(BIDS, subID, opt, info, session, run, 'bold'); FileName = fullfile(fileparts(mfilename('fullpath')), 'dummyData', ... 'derivatives', 'cpp_spm', ... diff --git a/tests/test_getMeanFuncFilename.m b/tests/test_getMeanFuncFilename.m index d57159ad..8c8a1272 100644 --- a/tests/test_getMeanFuncFilename.m +++ b/tests/test_getMeanFuncFilename.m @@ -8,18 +8,14 @@ function test_getMeanFuncFilenameBasic() - subID = '01'; - - opt.taskName = 'vislocalizer'; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.groups = {''}; - opt.subjects = {subID}; + subLabel = '01'; + opt = setOptions('vislocalizer', subLabel); opt = checkOptions(opt); - [~, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); - [meanImage, meanFuncDir] = getMeanFuncFilename(BIDS, subID, opt); + [meanImage, meanFuncDir] = getMeanFuncFilename(BIDS, subLabel, opt); expectedMeanImage = 'meanusub-01_ses-01_task-vislocalizer_bold.nii'; diff --git a/tests/test_getPrefix.m b/tests/test_getPrefix.m index dfbdeccb..83e5a3d8 100644 --- a/tests/test_getPrefix.m +++ b/tests/test_getPrefix.m @@ -10,6 +10,7 @@ function test_getPrefixSTC() step = 'realign'; funcFWHM = 6; + opt.metadata.SliceTiming = 1:0.2:1.8; opt.sliceOrder = 1:10; @@ -26,6 +27,7 @@ function test_getPrefixSTC() function test_getPrefixNoSTC() step = 'realign'; + opt.metadata = []; opt.sliceOrder = []; diff --git a/tests/test_getRFXdir.m b/tests/test_getRFXdir.m index 3473ae94..8e175fd4 100644 --- a/tests/test_getRFXdir.m +++ b/tests/test_getRFXdir.m @@ -11,9 +11,7 @@ function test_getRFXdirBasic() funcFWHM = 0; conFWHM = 0; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'funcLocalizer'; - + opt = setOptions('funcLocalizer'); opt = setDerivativesDir(opt); rfxDir = getRFXdir(opt, funcFWHM, conFWHM); diff --git a/tests/test_getRealignParamFile.m b/tests/test_getRealignParamFile.m index e4e66f66..185cf1f7 100644 --- a/tests/test_getRealignParamFile.m +++ b/tests/test_getRealignParamFile.m @@ -8,19 +8,16 @@ function test_getRealignParamFileBasic() - subID = '01'; + subLabel = '01'; session = '01'; run = ''; - opt.taskName = 'vislocalizer'; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.subjects = {subID}; - + opt = setOptions('vislocalizer', subLabel); opt = checkOptions(opt); - [~, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); - [boldFileName, subFuncDataDir] = getBoldFilename(BIDS, subID, session, run, opt); + [boldFileName, subFuncDataDir] = getBoldFilename(BIDS, subLabel, session, run, opt); realignParamFile = getRealignParamFile(fullfile(subFuncDataDir, boldFileName)); expectedFileName = fullfile(fileparts(mfilename('fullpath')), ... @@ -34,20 +31,17 @@ function test_getRealignParamFileBasic() function test_getRealignParamFileNativeSpace() - subID = '01'; + subLabel = '01'; session = '01'; run = ''; - opt.taskName = 'vislocalizer'; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.subjects = {subID}; + opt = setOptions('vislocalizer', subLabel); opt.space = 'individual'; - opt = checkOptions(opt); - [~, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); - [boldFileName, subFuncDataDir] = getBoldFilename(BIDS, subID, session, run, opt); + [boldFileName, subFuncDataDir] = getBoldFilename(BIDS, subLabel, session, run, opt); realignParamFile = getRealignParamFile(fullfile(subFuncDataDir, boldFileName)); expectedFileName = fullfile(fileparts(mfilename('fullpath')), ... @@ -61,21 +55,17 @@ function test_getRealignParamFileNativeSpace() function test_getRealignParamFileFFX() - subID = '01'; + subLabel = '01'; funcFWHM = 6; iSes = 1; iRun = 1; - opt.taskName = 'vislocalizer'; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.subjects = {subID}; - opt.space = 'MNI'; - + opt = setOptions('vislocalizer', subLabel); opt = checkOptions(opt); - [~, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); - [boldFileName, prefix] = getBoldFilenameForFFX(BIDS, opt, subID, funcFWHM, iSes, iRun); + [boldFileName, prefix] = getBoldFilenameForFFX(BIDS, opt, subLabel, funcFWHM, iSes, iRun); [subFuncDataDir, boldFileName, ext] = spm_fileparts(boldFileName); realignParamFile = getRealignParamFile(fullfile(subFuncDataDir, [boldFileName, ext]), prefix); diff --git a/tests/test_getSliceOrder.m b/tests/test_getSliceOrder.m index 25e06a14..6d590c6c 100644 --- a/tests/test_getSliceOrder.m +++ b/tests/test_getSliceOrder.m @@ -7,11 +7,8 @@ end function test_getSliceOrderBasic() - % Small test to ensure that getSliceOrder returns what we asked for - - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'vismotion'; + opt = setOptions('vismotion'); opt = checkOptions(opt); [~, opt] = getData(opt); @@ -40,10 +37,7 @@ function test_getSliceOrderBasic() function test_getSliceOrderEmpty() - %% Get empty slice order from BIDS - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'vislocalizer'; - + opt = setOptions('vislocalizer'); opt = checkOptions(opt); [~, opt] = getData(opt); @@ -56,12 +50,9 @@ function test_getSliceOrderEmpty() function test_getSliceOrderFromOptions() - %% Get slice order from options - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); + opt = setOptions('vislocalizer'); opt.STC_referenceSlice = 1000; opt.sliceOrder = 0:250:2000; - opt.taskName = 'vislocalizer'; - opt = checkOptions(opt); [~, opt] = getData(opt); diff --git a/tests/test_getSubjectList.m b/tests/test_getSubjectList.m index 25e0e12c..3ece0a6a 100644 --- a/tests/test_getSubjectList.m +++ b/tests/test_getSubjectList.m @@ -8,8 +8,8 @@ function test_getSubjectListNone() - opt = setOptions(); - + opt = setOptions('vismotion'); + opt = setDerivativesDir(opt); opt = checkOptions(opt); BIDS = bids.layout(opt.derivativesDir); @@ -24,8 +24,8 @@ function test_getSubjectListNone() function test_getSubjectListGroup() - opt = setOptions(); - + opt = setOptions('vismotion'); + opt = setDerivativesDir(opt); opt = checkOptions(opt); BIDS = bids.layout(opt.derivativesDir); @@ -43,8 +43,8 @@ function test_getSubjectListGroup() function test_getSubjectListBasic() - opt = setOptions(); - + opt = setOptions('vismotion'); + opt = setDerivativesDir(opt); opt = checkOptions(opt); BIDS = bids.layout(opt.derivativesDir); @@ -61,10 +61,8 @@ function test_getSubjectListBasic() function test_getSubjectListErrorSubject() - opt = setOptions(); - - opt.subjects = {'03'}; - + opt = setOptions('vismotion', '03'); + opt = setDerivativesDir(opt); opt = checkOptions(opt); BIDS = bids.layout(opt.derivativesDir); @@ -74,12 +72,3 @@ function test_getSubjectListErrorSubject() 'getSubjectList:noMatchingSubject'); end - -function opt = setOptions() - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), ... - 'dummyData', ... - 'derivatives', ... - 'cpp_spm'); - opt.taskName = 'vismotion'; - opt.zeropad = 2; -end diff --git a/tests/test_setBatchCoregistrationFuncToAnat.m b/tests/test_setBatchCoregistrationFuncToAnat.m index fe07cb59..def02014 100644 --- a/tests/test_setBatchCoregistrationFuncToAnat.m +++ b/tests/test_setBatchCoregistrationFuncToAnat.m @@ -11,20 +11,18 @@ function test_setBatchCoregistrationFuncToAnatBasic() % necessarry to deal with SPM module dependencies spm_jobman('initcfg'); - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'vismotion'; + subLabel = '02'; + opt = setOptions('vismotion', subLabel); opt = checkOptions(opt); - [~, opt, BIDS] = getData(opt); - - subID = '02'; + [BIDS, opt] = getData(opt); opt.orderBatches.selectAnat = 1; opt.orderBatches.realign = 2; matlabbatch = {}; - matlabbatch = setBatchCoregistrationFuncToAnat(matlabbatch, BIDS, opt, subID); + matlabbatch = setBatchCoregistrationFuncToAnat(matlabbatch, BIDS, opt, subLabel); nbRuns = 4; @@ -49,21 +47,20 @@ function test_setBatchCoregistrationFuncToAnatNoUnwarp() % necessarry to deal with SPM module dependencies spm_jobman('initcfg'); - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'vismotion'; - opt.realign.useUnwarp = false; + subLabel = '02'; + opt = setOptions('vismotion', subLabel); + opt = checkOptions(opt); + opt.realign.useUnwarp = false; opt = checkOptions(opt); - [~, opt, BIDS] = getData(opt); - - subID = '02'; + [BIDS, opt] = getData(opt); opt.orderBatches.selectAnat = 1; opt.orderBatches.realign = 2; matlabbatch = {}; - matlabbatch = setBatchCoregistrationFuncToAnat(matlabbatch, BIDS, opt, subID); + matlabbatch = setBatchCoregistrationFuncToAnat(matlabbatch, BIDS, opt, subLabel); nbRuns = 4; diff --git a/tests/test_setBatchFactorialDesign.m b/tests/test_setBatchFactorialDesign.m index f8b75325..c8f6bc3d 100644 --- a/tests/test_setBatchFactorialDesign.m +++ b/tests/test_setBatchFactorialDesign.m @@ -11,9 +11,8 @@ function test_setBatchFactorialDesignBasic() funcFWHM = 6; conFWHM = 6; + opt = setOptions('vismotion'); opt.subjects = {'01' '02'}; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'vismotion'; opt.model.file = fullfile(fileparts(mfilename('fullpath')), ... 'dummyData', 'models', 'model-visMotionLoc_smdl.json'); diff --git a/tests/test_setBatchMeanAnatAndMask.m b/tests/test_setBatchMeanAnatAndMask.m index 780567c4..9b777867 100644 --- a/tests/test_setBatchMeanAnatAndMask.m +++ b/tests/test_setBatchMeanAnatAndMask.m @@ -10,14 +10,10 @@ function test_setBatchMeanAnatAndMaskBasic() funcFWHM = 6; + opt = setOptions('vismotion'); opt.subjects = {'01', '02'}; - opt.taskName = 'vismotion'; - opt.space = 'MNI'; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), ... - 'dummyData', ... - 'derivatives', ... - 'cpp_spm'); + opt = setDerivativesDir(opt); opt = checkOptions(opt); matlabbatch = []; diff --git a/tests/test_setBatchRealign.m b/tests/test_setBatchRealign.m index 5be5d90f..dbd9d66e 100644 --- a/tests/test_setBatchRealign.m +++ b/tests/test_setBatchRealign.m @@ -14,18 +14,14 @@ function test_setBatchRealignBasic() % add test realign and unwarp % check it returns the right voxDim - opt.dataDir = fullfile(fileparts(mfilename('fullpath')), '..', 'demos', ... - 'MoAE', 'output', 'MoAEpilot'); - opt.taskName = 'auditory'; + subLabel = '01'; + opt = setOptions('MoAE', subLabel); opt = checkOptions(opt); - - [~, opt, BIDS] = getData(opt); - - subID = '01'; + [BIDS, opt] = getData(opt); matlabbatch = []; - matlabbatch = setBatchRealign(matlabbatch, BIDS, opt, subID); + matlabbatch = setBatchRealign(matlabbatch, BIDS, opt, subLabel); expectedBatch{1}.spm.spatial.realignunwarp.eoptions.weight = {''}; expectedBatch{end}.spm.spatial.realignunwarp.uwroptions.uwwhich = [2 1]; @@ -33,7 +29,7 @@ function test_setBatchRealignBasic() runCounter = 1; for iSes = 1 fileName = spm_BIDS(BIDS, 'data', ... - 'sub', subID, ... + 'sub', subLabel, ... 'task', opt.taskName, ... 'type', 'bold'); diff --git a/tests/test_setBatchSTC.m b/tests/test_setBatchSTC.m index 10dda843..7c054a90 100644 --- a/tests/test_setBatchSTC.m +++ b/tests/test_setBatchSTC.m @@ -8,16 +8,15 @@ function test_setBatchSTCEmpty() - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'vislocalizer'; + subLabel = '02'; + opt = setOptions('vislocalizer', subLabel); opt = checkOptions(opt); - [~, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); - subID = '02'; matlabbatch = []; - matlabbatch = setBatchSTC(matlabbatch, BIDS, opt, subID); + matlabbatch = setBatchSTC(matlabbatch, BIDS, opt, subLabel); % no slice timing info for this run so nothing should be returned. assertEqual(matlabbatch, []); @@ -26,8 +25,9 @@ function test_setBatchSTCEmpty() function test_setBatchSTCForce() - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'vislocalizer'; + subLabel = '02'; + + opt = setOptions('vislocalizer', subLabel); % we give it some slice timing value to force slice timing to happen opt.sliceOrder = linspace(0, 1.6, 10); opt.sliceOrder(end - 1:end) = []; @@ -35,12 +35,10 @@ function test_setBatchSTCForce() opt = checkOptions(opt); - [~, opt, BIDS] = getData(opt); - - subID = '02'; + [BIDS, opt] = getData(opt); matlabbatch = []; - matlabbatch = setBatchSTC(matlabbatch, BIDS, opt, subID); + matlabbatch = setBatchSTC(matlabbatch, BIDS, opt, subLabel); TR = 1.55; expectedBatch = returnExpectedBatch(opt.sliceOrder, opt.STC_referenceSlice, TR); @@ -48,7 +46,7 @@ function test_setBatchSTCForce() runCounter = 1; for iSes = 1:2 fileName = spm_BIDS(BIDS, 'data', ... - 'sub', subID, ... + 'sub', subLabel, ... 'ses', sprintf('0%i', iSes), ... 'task', opt.taskName, ... 'type', 'bold'); @@ -62,17 +60,16 @@ function test_setBatchSTCForce() function test_setBatchSTCBasic() - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'vismotion'; + subLabel = '02'; - opt = checkOptions(opt); + opt = setOptions('vismotion', subLabel); - [~, opt, BIDS] = getData(opt); + opt = checkOptions(opt); - subID = '02'; + [BIDS, opt] = getData(opt); matlabbatch = []; - matlabbatch = setBatchSTC(matlabbatch, BIDS, opt, subID); + matlabbatch = setBatchSTC(matlabbatch, BIDS, opt, subLabel); TR = 1.5; sliceOrder = repmat([ ... @@ -86,7 +83,7 @@ function test_setBatchSTCBasic() runCounter = 1; for iSes = 1:2 fileName = spm_BIDS(BIDS, 'data', ... - 'sub', subID, ... + 'sub', subLabel, ... 'ses', sprintf('0%i', iSes), ... 'task', opt.taskName, ... 'type', 'bold'); @@ -103,8 +100,9 @@ function test_setBatchSTCBasic() function test_setBatchSTCErrorInvalidInputTime() - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'vislocalizer'; + subLabel = '02'; + + opt = setOptions('vislocalizer', subLabel); opt.sliceOrder = linspace(0, 1.6, 10); opt.sliceOrder(end) = []; @@ -112,14 +110,12 @@ function test_setBatchSTCErrorInvalidInputTime() opt = checkOptions(opt); - subID = '02'; - - [~, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); matlabbatch = []; assertExceptionThrown( ... - @()setBatchSTC(matlabbatch, BIDS, opt, subID), ... + @()setBatchSTC(matlabbatch, BIDS, opt, subLabel), ... 'setBatchSTC:invalidInputTime'); end diff --git a/tests/test_setBatchSaveCoregistrationMatrix.m b/tests/test_setBatchSaveCoregistrationMatrix.m index 89889ce1..598eed1e 100644 --- a/tests/test_setBatchSaveCoregistrationMatrix.m +++ b/tests/test_setBatchSaveCoregistrationMatrix.m @@ -11,18 +11,18 @@ function test_setBatchSaveCoregistrationMatrixBasic() % necessarry to deal with SPM module dependencies spm_jobman('initcfg'); - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'vismotion'; + subLabel = '02'; + opt = setOptions('vismotion', subLabel); + opt = setDerivativesDir(opt); opt = checkOptions(opt); - [~, opt, BIDS] = getData(opt); - subID = '02'; + [BIDS, opt] = getData(opt); opt.orderBatches.coregister = 1; matlabbatch = {}; - matlabbatch = setBatchSaveCoregistrationMatrix(matlabbatch, BIDS, opt, subID); + matlabbatch = setBatchSaveCoregistrationMatrix(matlabbatch, BIDS, opt, subLabel); expectedBatch = returnExpectedBatch(); assertEqual(matlabbatch, expectedBatch); diff --git a/tests/test_setBatchSegmentation.m b/tests/test_setBatchSegmentation.m index 9e63d69a..2c50acd2 100644 --- a/tests/test_setBatchSegmentation.m +++ b/tests/test_setBatchSegmentation.m @@ -60,17 +60,17 @@ function test_setBatchSegmentationImages() function anatImage = returnLocalAnatFilename() - subID = '01'; + subLabel = '01'; opt.taskName = 'vislocalizer'; opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.subjects = {subID}; + opt.subjects = {subLabel}; opt = checkOptions(opt); - [~, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); - [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); + [anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt); anatImage = fullfile(anatDataDir, anatImage); diff --git a/tests/test_setBatchSelectAnat.m b/tests/test_setBatchSelectAnat.m index 14ba13c7..5e7ff3f7 100644 --- a/tests/test_setBatchSelectAnat.m +++ b/tests/test_setBatchSelectAnat.m @@ -12,18 +12,15 @@ function test_setBatchSelectAnatBasic() % add test to check if anat is not in first session % add test to check if anat is not a T1w - opt.dataDir = fullfile(fileparts(mfilename('fullpath')), '..', 'demos', ... - 'MoAE', 'output', 'MoAEpilot'); - opt.taskName = 'auditory'; + subLabel = '01'; + opt = setOptions('MoAE', subLabel); opt = checkOptions(opt); - [~, opt, BIDS] = getData(opt); - - subID = '01'; + [BIDS, opt] = getData(opt); matlabbatch = []; - matlabbatch = setBatchSelectAnat(matlabbatch, BIDS, opt, subID); + matlabbatch = setBatchSelectAnat(matlabbatch, BIDS, opt, subLabel); expectedBatch{1}.cfg_basicio.cfg_named_file.name = 'Anatomical'; diff --git a/tests/test_setBatchSkullStripping.m b/tests/test_setBatchSkullStripping.m index 51d5cba2..f6f88159 100644 --- a/tests/test_setBatchSkullStripping.m +++ b/tests/test_setBatchSkullStripping.m @@ -10,21 +10,17 @@ function test_setBatchSkullStrippingBasic() - subID = '01'; - - opt.taskName = 'vislocalizer'; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.groups = {''}; - opt.subjects = {subID}; + subLabel = '01'; + opt = setOptions('vislocalizer', subLabel); opt = checkOptions(opt); - [~, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); opt.orderBatches.segment = 2; matlabbatch = []; - matlabbatch = setBatchSkullStripping(matlabbatch, BIDS, opt, subID); + matlabbatch = setBatchSkullStripping(matlabbatch, BIDS, opt, subLabel); expectedBatch = returnExpectedBatch(opt); diff --git a/tests/test_setBatchSmoothConImages.m b/tests/test_setBatchSmoothConImages.m index 87cf445e..76091f34 100644 --- a/tests/test_setBatchSmoothConImages.m +++ b/tests/test_setBatchSmoothConImages.m @@ -8,22 +8,18 @@ function test_setBatchSmoothConImagesBasic() - opt.groups = {''}; - opt.subjects = {'01', '02'}; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), ... - 'dummyData', ... - 'derivatives', ... - 'cpp_spm'); - opt.taskName = 'vismotion'; - funcFWHM = 6; conFWHM = 6; + opt = setOptions('vismotion'); + opt.subjects = {'01', '02'}; + opt.taskName = 'vismotion'; opt = checkOptions(opt); - [group, opt] = getData(opt); + + [~, opt] = getData(opt); matlabbatch = []; - matlabbatch = setBatchSmoothConImages(matlabbatch, group, opt, funcFWHM, conFWHM); + matlabbatch = setBatchSmoothConImages(matlabbatch, opt, funcFWHM, conFWHM); expectedBatch{1}.spm.spatial.smooth.fwhm = [6 6 6]; expectedBatch{1}.spm.spatial.smooth.prefix = 's6'; diff --git a/tests/test_setBatchSmoothingFunc.m b/tests/test_setBatchSmoothingFunc.m index 0ea2ea31..bb9da2cd 100644 --- a/tests/test_setBatchSmoothingFunc.m +++ b/tests/test_setBatchSmoothingFunc.m @@ -11,21 +11,18 @@ function test_setBatchSmoothingFuncBasic() % TODO % need a test with several sessions and runs - subID = '01'; + subLabel = '01'; funcFWHM = 6; - opt.dataDir = fullfile(fileparts(mfilename('fullpath')), '..', 'demos', ... - 'MoAE', 'output', 'MoAEpilot'); - opt.taskName = 'auditory'; - + opt = setOptions('MoAE', subLabel); opt = checkOptions(opt); - [~, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); % create dummy normalized file fileName = spm_BIDS(BIDS, 'data', ... - 'sub', subID, ... + 'sub', subLabel, ... 'task', opt.taskName, ... 'type', 'bold'); [filepath, filename, ext] = fileparts(fileName{1}); @@ -37,7 +34,7 @@ function test_setBatchSmoothingFuncBasic() system(sprintf('touch %s', fileName)); matlabbatch = []; - matlabbatch = setBatchSmoothingFunc(matlabbatch, BIDS, opt, subID, funcFWHM); + matlabbatch = setBatchSmoothingFunc(matlabbatch, BIDS, opt, subLabel, funcFWHM); expectedBatch{1}.spm.spatial.smooth.fwhm = [6 6 6]; expectedBatch{1}.spm.spatial.smooth.dtype = 0; diff --git a/tests/test_setBatchSubjectLevelContrasts.m b/tests/test_setBatchSubjectLevelContrasts.m index 5fc7855c..35fa328a 100644 --- a/tests/test_setBatchSubjectLevelContrasts.m +++ b/tests/test_setBatchSubjectLevelContrasts.m @@ -8,23 +8,21 @@ function test_setBatchSubjectLevelContrastsBasic() - subID = '01'; + subLabel = '01'; funcFWHM = 6; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); + opt = setOptions('vismotion', subLabel); opt.space = 'MNI'; - opt.taskName = 'vismotion'; opt.model.file = ... fullfile(fileparts(mfilename('fullpath')), ... 'dummyData', ... 'models', ... 'model-visMotionLoc_smdl.json'); - opt = setDerivativesDir(opt); opt = checkOptions(opt); matlabbatch = []; - matlabbatch = setBatchSubjectLevelContrasts(matlabbatch, opt, subID, funcFWHM); + matlabbatch = setBatchSubjectLevelContrasts(matlabbatch, opt, subLabel, funcFWHM); expectedBatch = []; expectedBatch{end + 1}.spm.stats.con.spmmat = {fullfile(opt.derivativesDir, ... diff --git a/tests/test_setBatchSubjectLevelGLMSpec.m b/tests/test_setBatchSubjectLevelGLMSpec.m index d2a13cd2..e926deee 100644 --- a/tests/test_setBatchSubjectLevelGLMSpec.m +++ b/tests/test_setBatchSubjectLevelGLMSpec.m @@ -9,29 +9,23 @@ function test_setBatchSubjectLevelGLMSpecBasic() funcFWHM = 6; - subID = '01'; + subLabel = '01'; iSes = 1; iRun = 1; - opt.subjects = {subID}; - opt.taskName = 'auditory'; - opt.dataDir = fullfile( ... - fileparts(mfilename('fullpath')), ... - '..', 'demos', 'MoAE', 'output', 'MoAEpilot'); - opt.model.file = fullfile(fileparts(mfilename('fullpath')), ... - '..', 'demos', 'MoAE', 'models', 'model-MoAE_smdl.json'); + opt = setOptions('MoAE', subLabel); opt = checkOptions(opt); bidsCopyRawFolder(opt, 1); - [~, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); % create dummy preprocessed data - sessions = getInfo(BIDS, subID, opt, 'Sessions'); - runs = getInfo(BIDS, subID, opt, 'Runs', sessions{iSes}); + sessions = getInfo(BIDS, subLabel, opt, 'Sessions'); + runs = getInfo(BIDS, subLabel, opt, 'Runs', sessions{iSes}); [fileName, subFuncDataDir] = getBoldFilename( ... BIDS, ... - subID, sessions{iSes}, runs{iRun}, opt); + subLabel, sessions{iSes}, runs{iRun}, opt); copyfile(fullfile(subFuncDataDir, fileName), ... fullfile(subFuncDataDir, ['s6wu', fileName])); @@ -40,7 +34,7 @@ function test_setBatchSubjectLevelGLMSpecBasic() fullfile(subFuncDataDir, ['rp_', strrep(fileName, '.nii', '.txt')]))); matlabbatch = []; - matlabbatch = setBatchSubjectLevelGLMSpec(matlabbatch, BIDS, opt, subID, funcFWHM); + matlabbatch = setBatchSubjectLevelGLMSpec(matlabbatch, BIDS, opt, subLabel, funcFWHM); % TODO add assert % expectedBatch = returnExpectedBatch(); diff --git a/tests/test_setBatchSubjectLevelResults.m b/tests/test_setBatchSubjectLevelResults.m index b50a0b67..bd5eeb99 100644 --- a/tests/test_setBatchSubjectLevelResults.m +++ b/tests/test_setBatchSubjectLevelResults.m @@ -11,20 +11,18 @@ function test_setBatchSubjectLevelResultsBasic() iStep = 1; iCon = 1; - subID = '01'; + subLabel = '01'; funcFWHM = 6; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); + opt = setOptions('vismotion', subLabel); opt.space = 'MNI'; - opt.taskName = 'vismotion'; - opt = setDerivativesDir(opt); opt = checkOptions(opt); opt.result.Steps.Contrasts.Name = 'VisMot'; matlabbatch = []; - matlabbatch = setBatchSubjectLevelResults(matlabbatch, opt, subID, funcFWHM, iStep, iCon); + matlabbatch = setBatchSubjectLevelResults(matlabbatch, opt, subLabel, funcFWHM, iStep, iCon); expectedBatch = {}; @@ -56,13 +54,11 @@ function test_setBatchSubjectLevelResultsErrorMissingContrastName() iStep = 1; iCon = 1; - subID = '01'; + subLabel = '01'; funcFWHM = 6; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); + opt = setOptions('vismotion', subLabel); opt.space = 'MNI'; - opt.taskName = 'vismotion'; - opt = setDerivativesDir(opt); opt = checkOptions(opt); @@ -70,7 +66,7 @@ function test_setBatchSubjectLevelResultsErrorMissingContrastName() assertExceptionThrown( ... @()setBatchSubjectLevelResults(matlabbatch, ... opt, ... - subID, ... + subLabel, ... funcFWHM, ... iStep, ... iCon), ... @@ -83,23 +79,24 @@ function test_setBatchSubjectLevelResultsErrorNoMAtchingContrast() iStep = 1; iCon = 1; - subID = '01'; + subLabel = '01'; funcFWHM = 6; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.space = 'MNI'; - opt.taskName = 'vismotion'; - - opt.result.Steps.Contrasts.Name = 'NotAContrast'; + subLabel = '01'; + funcFWHM = 6; + opt = setOptions('vismotion', subLabel); + opt.space = 'MNI'; opt = setDerivativesDir(opt); opt = checkOptions(opt); + opt.result.Steps.Contrasts.Name = 'NotAContrast'; + matlabbatch = []; assertExceptionThrown( ... @()setBatchSubjectLevelResults(matlabbatch, ... opt, ... - subID, ... + subLabel, ... funcFWHM, ... iStep, ... iCon), ... diff --git a/tests/test_specifyContrasts.m b/tests/test_specifyContrasts.m index 5c3ff220..64d005e6 100644 --- a/tests/test_specifyContrasts.m +++ b/tests/test_specifyContrasts.m @@ -9,19 +9,18 @@ function test_specifyContrastsBasic() % Small test to ensure that pmCon returns what we asked for - subID = '01'; + subLabel = '01'; funcFWFM = 6; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.space = 'MNI'; - opt.taskName = 'vismotion'; + opt = setOptions('vismotion', subLabel); + opt = checkOptions(opt); + opt = setDerivativesDir(opt); + opt.model.file = ... fullfile(fileparts(mfilename('fullpath')), ... 'dummyData', 'models', 'model-visMotionLoc_smdl.json'); - opt = setDerivativesDir(opt); - - ffxDir = getFFXdir(subID, funcFWFM, opt); + ffxDir = getFFXdir(subLabel, funcFWFM, opt); contrasts = specifyContrasts(ffxDir, opt.taskName, opt); diff --git a/tests/utils/setOptions.m b/tests/utils/setOptions.m new file mode 100644 index 00000000..388fbfbb --- /dev/null +++ b/tests/utils/setOptions.m @@ -0,0 +1,22 @@ +function opt = setOptions(task, subLabel) + + if strcmp(task, 'MoAE') + + opt.dataDir = fullfile( ... + fileparts(mfilename('fullpath')), ... + '..', '..', 'demos', 'MoAE', 'output', 'MoAEpilot'); + + opt.taskName = 'auditory'; + + else + + opt.taskName = task; + opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), '..', 'dummyData'); + + end + + if nargin > 1 + opt.subjects = {subLabel}; + end + +end From a6cf65f8b6c924e65e4d36c9fd5b27d57363ec4b Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Tue, 9 Mar 2021 10:45:20 +0100 Subject: [PATCH 012/145] refactor tests --- tests/test_getInfo.m | 53 ++++++++++++++++++++-------------------- tests/test_setBatchSTC.m | 16 +++++++----- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/tests/test_getInfo.m b/tests/test_getInfo.m index fe4a973c..a00880ab 100644 --- a/tests/test_getInfo.m +++ b/tests/test_getInfo.m @@ -7,52 +7,40 @@ end function test_getInfoBasic() - % Small test to ensure that getSliceOrder returns what we asked for - % write tests for when no session or only one run - - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), ... - 'dummyData', 'derivatives', 'cpp_spm'); + subLabel = 'ctrl01'; + opt = setOptions('vismotion', subLabel); + opt = setDerivativesDir(opt); opt = checkOptions(opt); %% Get sessions from BIDS - opt.taskName = 'vismotion'; - subID = 'ctrl01'; info = 'sessions'; + [BIDS, opt] = getData(opt); - sessions = getInfo(BIDS, subID, opt, info); + sessions = getInfo(BIDS, subLabel, opt, info); assert(all(strcmp(sessions, {'01' '02'}))); %% Get runs from BIDS - opt.taskName = 'vismotion'; - subID = 'ctrl01'; info = 'runs'; - session = '01'; - [BIDS, opt] = getData(opt); - runs = getInfo(BIDS, subID, opt, info, session); - assert(all(strcmp(runs, {'1' '2'}))); - %% Get runs from BIDS when no run in filename - opt.taskName = 'vislocalizer'; - subID = 'ctrl01'; - info = 'runs'; session = '01'; + [BIDS, opt] = getData(opt); - runs = getInfo(BIDS, subID, opt, info, session); - assert(strcmp(runs, {''})); + runs = getInfo(BIDS, subLabel, opt, info, session); + assert(all(strcmp(runs, {'1' '2'}))); %% Get filename from BIDS - opt.taskName = 'vismotion'; - subID = 'ctrl01'; + info = 'filename'; + session = '01'; run = '1'; - info = 'filename'; + [BIDS, opt] = getData(opt); - filename = getInfo(BIDS, subID, opt, info, session, run, 'bold'); + filename = getInfo(BIDS, subLabel, opt, info, session, run, 'bold'); FileName = fullfile(fileparts(mfilename('fullpath')), 'dummyData', ... 'derivatives', 'cpp_spm', ... - ['sub-' subID], ['ses-' session], 'func', ... - ['sub-' subID, ... + ['sub-' subLabel], ['ses-' session], 'func', ... + ['sub-' subLabel, ... '_ses-' session, ... '_task-' opt.taskName, ... '_run-' run, ... @@ -60,4 +48,17 @@ function test_getInfoBasic() assert(strcmp(filename, FileName)); + %% Get runs from BIDS when no run in filename + subLabel = 'ctrl01'; + opt = setOptions('vislocalizer', subLabel); + opt = checkOptions(opt); + + info = 'runs'; + + session = '01'; + + [BIDS, opt] = getData(opt); + runs = getInfo(BIDS, subLabel, opt, info, session); + assert(strcmp(runs, {''})); + end diff --git a/tests/test_setBatchSTC.m b/tests/test_setBatchSTC.m index 7c054a90..cddce688 100644 --- a/tests/test_setBatchSTC.m +++ b/tests/test_setBatchSTC.m @@ -11,6 +11,7 @@ function test_setBatchSTCEmpty() subLabel = '02'; opt = setOptions('vislocalizer', subLabel); + opt = setDerivativesDir(opt); opt = checkOptions(opt); [BIDS, opt] = getData(opt); @@ -28,6 +29,8 @@ function test_setBatchSTCForce() subLabel = '02'; opt = setOptions('vislocalizer', subLabel); + opt = setDerivativesDir(opt); + % we give it some slice timing value to force slice timing to happen opt.sliceOrder = linspace(0, 1.6, 10); opt.sliceOrder(end - 1:end) = []; @@ -63,7 +66,7 @@ function test_setBatchSTCBasic() subLabel = '02'; opt = setOptions('vismotion', subLabel); - + opt = setDerivativesDir(opt); opt = checkOptions(opt); [BIDS, opt] = getData(opt); @@ -82,11 +85,11 @@ function test_setBatchSTCBasic() runCounter = 1; for iSes = 1:2 - fileName = spm_BIDS(BIDS, 'data', ... - 'sub', subLabel, ... - 'ses', sprintf('0%i', iSes), ... - 'task', opt.taskName, ... - 'type', 'bold'); + fileName = bids.query(BIDS, 'data', ... + 'sub', subLabel, ... + 'ses', sprintf('0%i', iSes), ... + 'task', opt.taskName, ... + 'type', 'bold'); expectedBatch{1}.spm.temporal.st.scans{runCounter} = ... {fileName{1}}; expectedBatch{1}.spm.temporal.st.scans{runCounter + 1} = ... @@ -103,6 +106,7 @@ function test_setBatchSTCErrorInvalidInputTime() subLabel = '02'; opt = setOptions('vislocalizer', subLabel); + opt = setDerivativesDir(opt); opt.sliceOrder = linspace(0, 1.6, 10); opt.sliceOrder(end) = []; From 6db0209678c6b8e58f80896f748e6e6741ce66e4 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Tue, 9 Mar 2021 11:31:12 +0100 Subject: [PATCH 013/145] clean code base - refactor - rename test functions - lint - add copyright --- src/getFuncVoxelDims.m | 17 +++++++++-------- src/getPrefix.m | 18 ++++++++---------- tests/miss_hit.cfg | 3 +-- tests/test_bidsCopyRawFolder.m | 2 ++ tests/test_checkOptions.m | 2 ++ tests/test_createAndReturnOnsetFile.m | 2 ++ tests/test_createDefaultModel.m | 2 ++ tests/test_getAnatFilename.m | 2 ++ tests/test_getBoldFilename.m | 2 ++ tests/test_getBoldFilenameForFFX.m | 2 ++ tests/test_getData.m | 2 ++ tests/test_getMeanFuncFilename.m | 2 ++ tests/test_getRealignParamFile.m | 2 ++ tests/test_loadAndCheckOptions.m | 2 ++ tests/test_modelFiles.m | 2 ++ tests/test_saveMatlabBatch.m | 2 ++ tests/test_setBatch3Dto4D.m | 2 ++ tests/test_setBatchComputeVDM.m | 2 ++ tests/test_setBatchCoregistrationFuncToAnat.m | 2 ++ tests/test_setBatchFactorialDesign.m | 2 ++ tests/test_setBatchImageCalculation.m | 2 ++ tests/test_setBatchMeanAnatAndMask.m | 2 ++ .../test_setBatchNormalizationSpatialPrepro.m | 2 ++ tests/test_setBatchRealign.m | 2 ++ tests/test_setBatchResults.m | 2 ++ tests/test_setBatchSTC.m | 2 ++ tests/test_setBatchSaveCoregistrationMatrix.m | 2 ++ tests/test_setBatchSegmentation.m | 2 ++ tests/test_setBatchSelectAnat.m | 2 ++ tests/test_setBatchSmoothConImages.m | 2 ++ tests/test_setBatchSmoothing.m | 2 ++ tests/test_setBatchSmoothingFunc.m | 2 ++ tests/test_setBatchSubjectLevelContrasts.m | 2 ++ tests/test_setBatchSubjectLevelGLMSpec.m | 2 ++ tests/test_setBatchSubjectLevelResults.m | 2 ++ tests/test_setDefaultFields.m | 2 ++ ...est_cleanCrash.m => test_unit_cleanCrash.m} | 4 +++- ...ut.m => test_unit_copyGraphWindownOutput.m} | 4 +++- ...nary.m => test_unit_createDataDictionary.m} | 4 +++- ...{test_getFFXdir.m => test_unit_getFFXdir.m} | 2 ++ ...oxelDims.m => test_unit_getFuncVoxelDims.m} | 4 +++- ...> test_unit_getGrpLevelContrastToCompute.m} | 4 +++- tests/{test_getInfo.m => test_unit_getInfo.m} | 4 +++- ...{test_getPrefix.m => test_unit_getPrefix.m} | 4 +++- ...{test_getRFXdir.m => test_unit_getRFXdir.m} | 4 +++- ...tSliceOrder.m => test_unit_getSliceOrder.m} | 4 +++- ...ubjectList.m => test_unit_getSubjectList.m} | 4 +++- ...ersPool.m => test_unit_manageWorkersPool.m} | 4 +++- ... test_unit_returnDefaultResultsStructure.m} | 2 +- ...ptyModel.m => test_unit_returnEmptyModel.m} | 4 ++-- ...ivesDir.m => test_unit_setDerivativesDir.m} | 4 +++- ...ontrasts.m => test_unit_specifyContrasts.m} | 4 +++- ...tFile.m => test_unit_validationInputFile.m} | 4 +++- tests/test_utils.m | 2 ++ tests/test_writeDatasetDescription.m | 2 ++ tests/utils/setOptions.m | 2 ++ 56 files changed, 137 insertions(+), 37 deletions(-) rename tests/{test_cleanCrash.m => test_unit_cleanCrash.m} (81%) rename tests/{test_copyGraphWindownOutput.m => test_unit_copyGraphWindownOutput.m} (93%) rename tests/{test_createDataDictionary.m => test_unit_createDataDictionary.m} (91%) rename tests/{test_getFFXdir.m => test_unit_getFFXdir.m} (95%) rename tests/{test_getFuncVoxelDims.m => test_unit_getFuncVoxelDims.m} (90%) rename tests/{test_getGrpLevelContrastToCompute.m => test_unit_getGrpLevelContrastToCompute.m} (90%) rename tests/{test_getInfo.m => test_unit_getInfo.m} (93%) rename tests/{test_getPrefix.m => test_unit_getPrefix.m} (98%) rename tests/{test_getRFXdir.m => test_unit_getRFXdir.m} (88%) rename tests/{test_getSliceOrder.m => test_unit_getSliceOrder.m} (92%) rename tests/{test_getSubjectList.m => test_unit_getSubjectList.m} (93%) rename tests/{test_manageWorkersPool.m => test_unit_manageWorkersPool.m} (91%) rename tests/{test_returnDefaultResultsStructure.m => test_unit_returnDefaultResultsStructure.m} (93%) rename tests/{test_returnEmptyModel.m => test_unit_returnEmptyModel.m} (83%) rename tests/{test_setDerivativesDir.m => test_unit_setDerivativesDir.m} (91%) rename tests/{test_specifyContrasts.m => test_unit_specifyContrasts.m} (90%) rename tests/{test_validationInputFile.m => test_unit_validationInputFile.m} (90%) diff --git a/src/getFuncVoxelDims.m b/src/getFuncVoxelDims.m index 99c825b2..296398fc 100644 --- a/src/getFuncVoxelDims.m +++ b/src/getFuncVoxelDims.m @@ -6,19 +6,20 @@ % % USAGE:: % - % [argout1, argout2] = templateFunction(argin1, [argin2 == default,] [argin3]) + % [voxDim, opt] = getFuncVoxelDims(opt, subFuncDataDir, prefix, fileName) % % :param opt: Options chosen for the analysis. See ``checkOptions()``. % :type opt: structure - % :param argin2: optional argument and its default value. And some of the - % options can be shown in litteral like ``this`` or ``that``. - % :type argin2: string - % :param argin3: (dimension) optional argument + % :param subFuncDataDir: + % :type subFuncDataDir: + % :param prefix: + % :type prefix: + % :param fileName: + % :type fileName: % - % :returns: - :argout1: (type) (dimension) - % - :argout2: (type) (dimension) + % :returns: - :voxDim: + % - :opt: % - % [voxDim, opt] = getFuncVoxelDims(opt, subFuncDataDir, prefix, fileName) % % diff --git a/src/getPrefix.m b/src/getPrefix.m index e99d41cc..5c8c334b 100644 --- a/src/getPrefix.m +++ b/src/getPrefix.m @@ -2,25 +2,23 @@ function [prefix, motionRegressorPrefix] = getPrefix(step, opt, funcFWHM) % - % Short description of what the function does goes here. + % Generates prefix to append to file name to look for % % USAGE:: % - % [argout1, argout2] = templateFunction(argin1, [argin2 == default,] [argin3]) + % [prefix, motionRegressorPrefix] = getPrefix(step, opt, funcFWHM) % - % :param argin1: (dimension) obligatory argument. Lorem ipsum dolor sit amet, - % consectetur adipiscing elit. Ut congue nec est ac lacinia. - % :type argin1: type + % :param step: + % :type step: string % :param opt: Options chosen for the analysis. See ``checkOptions()``. % :type opt: structure - % :param argin3: (dimension) optional argument + % :param funcFWHM: + % :type funcFWHM: scalar % - % :returns: - :argout1: (type) (dimension) - % - :argout2: (type) (dimension) + % :returns: - :prefix: + % - :motionRegressorPrefix: % - % [prefix, motionRegressorPrefix] = getPrefix(step, opt, funcFWHM) % - % generates prefix to append to file name to look for if nargin < 3 funcFWHM = 0; diff --git a/tests/miss_hit.cfg b/tests/miss_hit.cfg index 1aa3765a..d4c4fc66 100644 --- a/tests/miss_hit.cfg +++ b/tests/miss_hit.cfg @@ -1,7 +1,6 @@ # style guide (https://florianschanda.github.io/miss_hit/style_checker.html) line_length: 100 -regex_function_name: "((test_[a-z]+)|[a-z]+)(([A-Z]){1}[A-Za-z0-9]+)*" -suppress_rule: "copyright_notice" +regex_function_name: "(test(_unit){0,1}_[a-z]+|[a-z]+)(([A-Z]){1}[A-Za-z0-9]+)*" # metrics limit for the code quality (https://florianschanda.github.io/miss_hit/metrics.html) metric "cnest": limit 4 diff --git a/tests/test_bidsCopyRawFolder.m b/tests/test_bidsCopyRawFolder.m index d6e7d215..523c96bb 100644 --- a/tests/test_bidsCopyRawFolder.m +++ b/tests/test_bidsCopyRawFolder.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_bidsCopyRawFolder %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_checkOptions.m b/tests/test_checkOptions.m index 158e4d40..6390bfa0 100644 --- a/tests/test_checkOptions.m +++ b/tests/test_checkOptions.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_checkOptions %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_createAndReturnOnsetFile.m b/tests/test_createAndReturnOnsetFile.m index 492dafc8..493b6dfe 100644 --- a/tests/test_createAndReturnOnsetFile.m +++ b/tests/test_createAndReturnOnsetFile.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_createAndReturnOnsetFile %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_createDefaultModel.m b/tests/test_createDefaultModel.m index 77d878c1..f95e98a3 100644 --- a/tests/test_createDefaultModel.m +++ b/tests/test_createDefaultModel.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_createDefaultModel %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_getAnatFilename.m b/tests/test_getAnatFilename.m index 92b73333..a2e04330 100644 --- a/tests/test_getAnatFilename.m +++ b/tests/test_getAnatFilename.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_getAnatFilename %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_getBoldFilename.m b/tests/test_getBoldFilename.m index bd175ffc..cb55235c 100644 --- a/tests/test_getBoldFilename.m +++ b/tests/test_getBoldFilename.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_getBoldFilename %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_getBoldFilenameForFFX.m b/tests/test_getBoldFilenameForFFX.m index 2ca8f48e..f677e761 100644 --- a/tests/test_getBoldFilenameForFFX.m +++ b/tests/test_getBoldFilenameForFFX.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_getBoldFilenameForFFX %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_getData.m b/tests/test_getData.m index 64a134d5..dfa32545 100644 --- a/tests/test_getData.m +++ b/tests/test_getData.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_getData %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_getMeanFuncFilename.m b/tests/test_getMeanFuncFilename.m index 8c8a1272..623c1ec1 100644 --- a/tests/test_getMeanFuncFilename.m +++ b/tests/test_getMeanFuncFilename.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_getMeanFuncFilename %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_getRealignParamFile.m b/tests/test_getRealignParamFile.m index 185cf1f7..9ebce4ea 100644 --- a/tests/test_getRealignParamFile.m +++ b/tests/test_getRealignParamFile.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_getRealignParamFile %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_loadAndCheckOptions.m b/tests/test_loadAndCheckOptions.m index dd35d983..e0a4cb07 100644 --- a/tests/test_loadAndCheckOptions.m +++ b/tests/test_loadAndCheckOptions.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_loadAndCheckOptions %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_modelFiles.m b/tests/test_modelFiles.m index 4c442b60..1a80e561 100644 --- a/tests/test_modelFiles.m +++ b/tests/test_modelFiles.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_modelFiles %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_saveMatlabBatch.m b/tests/test_saveMatlabBatch.m index a05c72a7..af8327e7 100644 --- a/tests/test_saveMatlabBatch.m +++ b/tests/test_saveMatlabBatch.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_saveMatlabBatch %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_setBatch3Dto4D.m b/tests/test_setBatch3Dto4D.m index 6a169d3e..53d3eded 100644 --- a/tests/test_setBatch3Dto4D.m +++ b/tests/test_setBatch3Dto4D.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_setBatch3Dto4D %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_setBatchComputeVDM.m b/tests/test_setBatchComputeVDM.m index 64774ded..1f46e1a5 100644 --- a/tests/test_setBatchComputeVDM.m +++ b/tests/test_setBatchComputeVDM.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_setBatchComputeVDM %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_setBatchCoregistrationFuncToAnat.m b/tests/test_setBatchCoregistrationFuncToAnat.m index def02014..20c163eb 100644 --- a/tests/test_setBatchCoregistrationFuncToAnat.m +++ b/tests/test_setBatchCoregistrationFuncToAnat.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_setBatchCoregistrationFuncToAnat %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_setBatchFactorialDesign.m b/tests/test_setBatchFactorialDesign.m index c8f6bc3d..5cbb1fef 100644 --- a/tests/test_setBatchFactorialDesign.m +++ b/tests/test_setBatchFactorialDesign.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_setBatchFactorialDesign %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_setBatchImageCalculation.m b/tests/test_setBatchImageCalculation.m index 235d27a4..bc21e0db 100644 --- a/tests/test_setBatchImageCalculation.m +++ b/tests/test_setBatchImageCalculation.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_setBatchImageCalculation %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_setBatchMeanAnatAndMask.m b/tests/test_setBatchMeanAnatAndMask.m index 9b777867..36b8f0c1 100644 --- a/tests/test_setBatchMeanAnatAndMask.m +++ b/tests/test_setBatchMeanAnatAndMask.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_setBatchMeanAnatAndMask %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_setBatchNormalizationSpatialPrepro.m b/tests/test_setBatchNormalizationSpatialPrepro.m index 7a3199a5..ca36ac08 100644 --- a/tests/test_setBatchNormalizationSpatialPrepro.m +++ b/tests/test_setBatchNormalizationSpatialPrepro.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_setBatchNormalizationSpatialPrepro %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_setBatchRealign.m b/tests/test_setBatchRealign.m index dbd9d66e..abccf14b 100644 --- a/tests/test_setBatchRealign.m +++ b/tests/test_setBatchRealign.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_setBatchRealign %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_setBatchResults.m b/tests/test_setBatchResults.m index ca500734..1a39cfa5 100644 --- a/tests/test_setBatchResults.m +++ b/tests/test_setBatchResults.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_setBatchResults %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_setBatchSTC.m b/tests/test_setBatchSTC.m index cddce688..39fc736d 100644 --- a/tests/test_setBatchSTC.m +++ b/tests/test_setBatchSTC.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_setBatchSTC %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_setBatchSaveCoregistrationMatrix.m b/tests/test_setBatchSaveCoregistrationMatrix.m index 598eed1e..dda848aa 100644 --- a/tests/test_setBatchSaveCoregistrationMatrix.m +++ b/tests/test_setBatchSaveCoregistrationMatrix.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_setBatchSaveCoregistrationMatrix %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_setBatchSegmentation.m b/tests/test_setBatchSegmentation.m index 2c50acd2..7304369b 100644 --- a/tests/test_setBatchSegmentation.m +++ b/tests/test_setBatchSegmentation.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_setBatchSegmentation %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_setBatchSelectAnat.m b/tests/test_setBatchSelectAnat.m index 5e7ff3f7..38023a7c 100644 --- a/tests/test_setBatchSelectAnat.m +++ b/tests/test_setBatchSelectAnat.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_setBatchSelectAnat %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_setBatchSmoothConImages.m b/tests/test_setBatchSmoothConImages.m index 76091f34..6579c4ce 100644 --- a/tests/test_setBatchSmoothConImages.m +++ b/tests/test_setBatchSmoothConImages.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_setBatchSmoothConImages %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_setBatchSmoothing.m b/tests/test_setBatchSmoothing.m index 982219e3..ec5f0e2d 100644 --- a/tests/test_setBatchSmoothing.m +++ b/tests/test_setBatchSmoothing.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_setBatchSmoothing %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_setBatchSmoothingFunc.m b/tests/test_setBatchSmoothingFunc.m index bb9da2cd..66b6f60f 100644 --- a/tests/test_setBatchSmoothingFunc.m +++ b/tests/test_setBatchSmoothingFunc.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_setBatchSmoothingFunc %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_setBatchSubjectLevelContrasts.m b/tests/test_setBatchSubjectLevelContrasts.m index 35fa328a..5da6d60b 100644 --- a/tests/test_setBatchSubjectLevelContrasts.m +++ b/tests/test_setBatchSubjectLevelContrasts.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_setBatchSubjectLevelContrasts %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_setBatchSubjectLevelGLMSpec.m b/tests/test_setBatchSubjectLevelGLMSpec.m index e926deee..0d75c938 100644 --- a/tests/test_setBatchSubjectLevelGLMSpec.m +++ b/tests/test_setBatchSubjectLevelGLMSpec.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_setBatchSubjectLevelGLMSpec %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_setBatchSubjectLevelResults.m b/tests/test_setBatchSubjectLevelResults.m index bd5eeb99..d6dce7b0 100644 --- a/tests/test_setBatchSubjectLevelResults.m +++ b/tests/test_setBatchSubjectLevelResults.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_setBatchSubjectLevelResults %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_setDefaultFields.m b/tests/test_setDefaultFields.m index 548101ae..3debd2bb 100644 --- a/tests/test_setDefaultFields.m +++ b/tests/test_setDefaultFields.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_setDefaultFields %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_cleanCrash.m b/tests/test_unit_cleanCrash.m similarity index 81% rename from tests/test_cleanCrash.m rename to tests/test_unit_cleanCrash.m index d1781f88..ae8cbfbe 100644 --- a/tests/test_cleanCrash.m +++ b/tests/test_unit_cleanCrash.m @@ -1,4 +1,6 @@ -function test_suite = test_cleanCrash %#ok<*STOUT> +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + +function test_suite = test_unit_cleanCrash %#ok<*STOUT> 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/test_copyGraphWindownOutput.m b/tests/test_unit_copyGraphWindownOutput.m similarity index 93% rename from tests/test_copyGraphWindownOutput.m rename to tests/test_unit_copyGraphWindownOutput.m index 0958cc5a..b0698f7b 100644 --- a/tests/test_copyGraphWindownOutput.m +++ b/tests/test_unit_copyGraphWindownOutput.m @@ -1,4 +1,6 @@ -function test_suite = test_copyGraphWindownOutput %#ok<*STOUT> +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + +function test_suite = test_unit_copyGraphWindownOutput %#ok<*STOUT> 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/test_createDataDictionary.m b/tests/test_unit_createDataDictionary.m similarity index 91% rename from tests/test_createDataDictionary.m rename to tests/test_unit_createDataDictionary.m index f33d63c1..4cc3bdd0 100644 --- a/tests/test_createDataDictionary.m +++ b/tests/test_unit_createDataDictionary.m @@ -1,4 +1,6 @@ -function test_suite = test_createDataDictionary %#ok<*STOUT> +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + +function test_suite = test_unit_createDataDictionary %#ok<*STOUT> 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/test_getFFXdir.m b/tests/test_unit_getFFXdir.m similarity index 95% rename from tests/test_getFFXdir.m rename to tests/test_unit_getFFXdir.m index 0fddb075..7eb67e59 100644 --- a/tests/test_getFFXdir.m +++ b/tests/test_unit_getFFXdir.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_getFFXdir %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_getFuncVoxelDims.m b/tests/test_unit_getFuncVoxelDims.m similarity index 90% rename from tests/test_getFuncVoxelDims.m rename to tests/test_unit_getFuncVoxelDims.m index f7800da6..0527b2a4 100644 --- a/tests/test_getFuncVoxelDims.m +++ b/tests/test_unit_getFuncVoxelDims.m @@ -1,4 +1,6 @@ -function test_suite = test_getFuncVoxelDims %#ok<*STOUT> +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + +function test_suite = test_unit_getFuncVoxelDims %#ok<*STOUT> 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/test_getGrpLevelContrastToCompute.m b/tests/test_unit_getGrpLevelContrastToCompute.m similarity index 90% rename from tests/test_getGrpLevelContrastToCompute.m rename to tests/test_unit_getGrpLevelContrastToCompute.m index 6ac688d0..7b1723c5 100644 --- a/tests/test_getGrpLevelContrastToCompute.m +++ b/tests/test_unit_getGrpLevelContrastToCompute.m @@ -1,4 +1,6 @@ -function test_suite = test_getGrpLevelContrastToCompute %#ok<*STOUT> +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + +function test_suite = test_unit_getGrpLevelContrastToCompute %#ok<*STOUT> 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/test_getInfo.m b/tests/test_unit_getInfo.m similarity index 93% rename from tests/test_getInfo.m rename to tests/test_unit_getInfo.m index a00880ab..eed1dcfa 100644 --- a/tests/test_getInfo.m +++ b/tests/test_unit_getInfo.m @@ -1,4 +1,6 @@ -function test_suite = test_getInfo %#ok<*STOUT> +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + +function test_suite = test_unit_getInfo %#ok<*STOUT> 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/test_getPrefix.m b/tests/test_unit_getPrefix.m similarity index 98% rename from tests/test_getPrefix.m rename to tests/test_unit_getPrefix.m index 83e5a3d8..27e2e50f 100644 --- a/tests/test_getPrefix.m +++ b/tests/test_unit_getPrefix.m @@ -1,4 +1,6 @@ -function test_suite = test_getPrefix %#ok<*STOUT> +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + +function test_suite = test_unit_getPrefix %#ok<*STOUT> 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/test_getRFXdir.m b/tests/test_unit_getRFXdir.m similarity index 88% rename from tests/test_getRFXdir.m rename to tests/test_unit_getRFXdir.m index 8e175fd4..cbdaadec 100644 --- a/tests/test_getRFXdir.m +++ b/tests/test_unit_getRFXdir.m @@ -1,4 +1,6 @@ -function test_suite = test_getRFXdir %#ok<*STOUT> +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + +function test_suite = test_unit_getRFXdir %#ok<*STOUT> 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/test_getSliceOrder.m b/tests/test_unit_getSliceOrder.m similarity index 92% rename from tests/test_getSliceOrder.m rename to tests/test_unit_getSliceOrder.m index 6d590c6c..12f55d2a 100644 --- a/tests/test_getSliceOrder.m +++ b/tests/test_unit_getSliceOrder.m @@ -1,4 +1,6 @@ -function test_suite = test_getSliceOrder %#ok<*STOUT> +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + +function test_suite = test_unit_getSliceOrder %#ok<*STOUT> 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/test_getSubjectList.m b/tests/test_unit_getSubjectList.m similarity index 93% rename from tests/test_getSubjectList.m rename to tests/test_unit_getSubjectList.m index 3ece0a6a..4d4a6e5b 100644 --- a/tests/test_getSubjectList.m +++ b/tests/test_unit_getSubjectList.m @@ -1,4 +1,6 @@ -function test_suite = test_getSubjectList %#ok<*STOUT> +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + +function test_suite = test_unit_getSubjectList %#ok<*STOUT> 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/test_manageWorkersPool.m b/tests/test_unit_manageWorkersPool.m similarity index 91% rename from tests/test_manageWorkersPool.m rename to tests/test_unit_manageWorkersPool.m index 9754a9ea..b7c6c6f8 100644 --- a/tests/test_manageWorkersPool.m +++ b/tests/test_unit_manageWorkersPool.m @@ -1,4 +1,6 @@ -function test_suite = test_manageWorkersPool %#ok<*STOUT> +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + +function test_suite = test_unit_manageWorkersPool %#ok<*STOUT> 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/test_returnDefaultResultsStructure.m b/tests/test_unit_returnDefaultResultsStructure.m similarity index 93% rename from tests/test_returnDefaultResultsStructure.m rename to tests/test_unit_returnDefaultResultsStructure.m index 75cd688c..8ef9ec10 100644 --- a/tests/test_returnDefaultResultsStructure.m +++ b/tests/test_unit_returnDefaultResultsStructure.m @@ -1,6 +1,6 @@ % (C) Copyright 2020 CPP BIDS SPM-pipeline developers -function test_suite = test_returnDefaultResultsStructure %#ok<*STOUT> +function test_suite = test_unit_returnDefaultResultsStructure %#ok<*STOUT> 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/test_returnEmptyModel.m b/tests/test_unit_returnEmptyModel.m similarity index 83% rename from tests/test_returnEmptyModel.m rename to tests/test_unit_returnEmptyModel.m index 343ee25b..cb006589 100644 --- a/tests/test_returnEmptyModel.m +++ b/tests/test_unit_returnEmptyModel.m @@ -1,6 +1,6 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers -function test_suite = test_returnEmptyModel %#ok<*STOUT> +function test_suite = test_unit_returnEmptyModel %#ok<*STOUT> 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/test_setDerivativesDir.m b/tests/test_unit_setDerivativesDir.m similarity index 91% rename from tests/test_setDerivativesDir.m rename to tests/test_unit_setDerivativesDir.m index 4afab5cb..4df4a2cc 100644 --- a/tests/test_setDerivativesDir.m +++ b/tests/test_unit_setDerivativesDir.m @@ -1,4 +1,6 @@ -function test_suite = test_setDerivativesDir %#ok<*STOUT> +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + +function test_suite = test_unit_setDerivativesDir %#ok<*STOUT> 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/test_specifyContrasts.m b/tests/test_unit_specifyContrasts.m similarity index 90% rename from tests/test_specifyContrasts.m rename to tests/test_unit_specifyContrasts.m index 64d005e6..45fb979c 100644 --- a/tests/test_specifyContrasts.m +++ b/tests/test_unit_specifyContrasts.m @@ -1,4 +1,6 @@ -function test_suite = test_specifyContrasts %#ok<*STOUT> +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + +function test_suite = test_unit_specifyContrasts %#ok<*STOUT> 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/test_validationInputFile.m b/tests/test_unit_validationInputFile.m similarity index 90% rename from tests/test_validationInputFile.m rename to tests/test_unit_validationInputFile.m index b72e8413..29ad4135 100644 --- a/tests/test_validationInputFile.m +++ b/tests/test_unit_validationInputFile.m @@ -1,4 +1,6 @@ -function test_suite = test_validationInputFile %#ok<*STOUT> +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + +function test_suite = test_unit_validationInputFile %#ok<*STOUT> 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/test_utils.m b/tests/test_utils.m index 4418b9cc..ec276918 100644 --- a/tests/test_utils.m +++ b/tests/test_utils.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_utils %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/test_writeDatasetDescription.m b/tests/test_writeDatasetDescription.m index cbb964ff..a88804f7 100644 --- a/tests/test_writeDatasetDescription.m +++ b/tests/test_writeDatasetDescription.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP BIDS SPM-pipeline developers + function test_suite = test_writeDatasetDescription %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> diff --git a/tests/utils/setOptions.m b/tests/utils/setOptions.m index 388fbfbb..3e3c43c6 100644 --- a/tests/utils/setOptions.m +++ b/tests/utils/setOptions.m @@ -1,3 +1,5 @@ +% (C) Copyright 2021 CPP BIDS SPM-pipeline developers + function opt = setOptions(task, subLabel) if strcmp(task, 'MoAE') From 4d5b2f7980bb64792c48f3497b434a80f444cb57 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Tue, 9 Mar 2021 12:49:06 +0100 Subject: [PATCH 014/145] fix tests --- src/batches/setBatchFactorialDesign.m | 33 ++++++++++-------------- tests/test_setBatchSubjectLevelGLMSpec.m | 1 + tests/utils/setOptions.m | 3 +++ 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/batches/setBatchFactorialDesign.m b/src/batches/setBatchFactorialDesign.m index c766a6b8..ec5c9262 100644 --- a/src/batches/setBatchFactorialDesign.m +++ b/src/batches/setBatchFactorialDesign.m @@ -54,32 +54,27 @@ mkdir(directory); - % For each group - for iGroup = 1:length(opt.groups) + icell(1).levels = 1; %#ok<*AGROW> - icell(iGroup).levels = iGroup; %#ok<*AGROW> + for iSub = 1:numel(opt.subjects) - for iSub = 1:group(iGroup).numSub + subLabel = opt.subjects{iSub}; - subLabel = group(iGroup).subNumber{iSub}; + printProcessingSubject(iSub, subLabel); - printProcessingSubject(iSub, subLabel); + % FFX directory and load SPM.mat of that subject + ffxDir = getFFXdir(subLabel, funcFWHM, opt); + load(fullfile(ffxDir, 'SPM.mat')); - % FFX directory and load SPM.mat of that subject - ffxDir = getFFXdir(subLabel, funcFWHM, opt); - load(fullfile(ffxDir, 'SPM.mat')); + % find which contrast of that subject has the name of the contrast we + % want to bring to the group level + conIdx = find(strcmp({SPM.xCon.name}, conName)); + fileName = sprintf('con_%0.4d.nii', conIdx); + file = validationInputFile(ffxDir, fileName, smoothPrefix); - % find which contrast of that subject has the name of the contrast we - % want to bring to the group level - conIdx = find(strcmp({SPM.xCon.name}, conName)); - fileName = sprintf('con_%0.4d.nii', conIdx); - file = validationInputFile(ffxDir, fileName, smoothPrefix); + icell(1).scans(iSub, :) = {file}; - icell(iGroup).scans(iSub, :) = {file}; - - fprintf(1, ' %s\n\n', file); - - end + fprintf(1, ' %s\n\n', file); end diff --git a/tests/test_setBatchSubjectLevelGLMSpec.m b/tests/test_setBatchSubjectLevelGLMSpec.m index 0d75c938..ec2305f0 100644 --- a/tests/test_setBatchSubjectLevelGLMSpec.m +++ b/tests/test_setBatchSubjectLevelGLMSpec.m @@ -16,6 +16,7 @@ function test_setBatchSubjectLevelGLMSpecBasic() iRun = 1; opt = setOptions('MoAE', subLabel); + opt = checkOptions(opt); bidsCopyRawFolder(opt, 1); diff --git a/tests/utils/setOptions.m b/tests/utils/setOptions.m index 3e3c43c6..b9c9c4d3 100644 --- a/tests/utils/setOptions.m +++ b/tests/utils/setOptions.m @@ -7,6 +7,9 @@ opt.dataDir = fullfile( ... fileparts(mfilename('fullpath')), ... '..', '..', 'demos', 'MoAE', 'output', 'MoAEpilot'); + opt.model.file = fullfile( ... + fileparts(mfilename('fullpath')), ... + '..', '..', 'demos', 'MoAE', 'models', 'model-MoAE_smdl.json'); opt.taskName = 'auditory'; From 1f51b078bdb213daa5287c4589b8bac8fc847bfd Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Tue, 9 Mar 2021 13:10:05 +0100 Subject: [PATCH 015/145] fixes #275 --- src/batches/setBatchSTC.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/batches/setBatchSTC.m b/src/batches/setBatchSTC.m index 9c44dca5..63595245 100644 --- a/src/batches/setBatchSTC.m +++ b/src/batches/setBatchSTC.m @@ -49,6 +49,10 @@ nbSlices = length(sliceOrder); % unique is necessary in case of multi echo TR = opt.metadata.RepetitionTime; TA = TR - (TR / nbSlices); + % round acquisition time to the upper millisecond + % mostly to avoid having errors when checking: + % any(sliceOrder > TA) + TA = ceil(TA * 1000)/1000; maxSliceTime = max(sliceOrder); minSliceTime = min(sliceOrder); From 212fd0cb8c848dc3e43fa7e3bdfda42ea34cfde4 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Tue, 9 Mar 2021 13:11:40 +0100 Subject: [PATCH 016/145] fixes QA pipelines --- src/QA/anatomicalQA.m | 44 +++++------- src/QA/functionalQA.m | 145 ++++++++++++++++++-------------------- src/batches/setBatchSTC.m | 2 +- 3 files changed, 89 insertions(+), 102 deletions(-) diff --git a/src/QA/anatomicalQA.m b/src/QA/anatomicalQA.m index 25d70fa9..c147602d 100644 --- a/src/QA/anatomicalQA.m +++ b/src/QA/anatomicalQA.m @@ -23,43 +23,37 @@ function anatomicalQA(opt) end opt = loadAndCheckOptions(opt); - [group, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); fprintf(1, ' ANATOMICAL: QUALITY CONTROL\n\n'); - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) + parfor iSub = 1:numel(opt.subjects) - groupName = group(iGroup).name; + subID = opt.subjects{iSub}; - parfor iSub = 1:group(iGroup).numSub + printProcessingSubject(iSub, subID); - subID = group(iGroup).subNumber{iSub}; + [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); - printProcessingSubject(groupName, iSub, subID); + % get grey and white matter tissue probability maps + TPMs = validationInputFile(anatDataDir, anatImage, 'c[12]'); - [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); + % sanity check that all images are in the same space. + anatImage = fullfile(anatDataDir, anatImage); + volumesToCheck = {anatImage; TPMs(1, :); TPMs(2, :)}; + spm_check_orientations(spm_vol(char(volumesToCheck))); - % get grey and white matter tissue probability maps - TPMs = validationInputFile(anatDataDir, anatImage, 'c[12]'); + % Basic QA for anatomical data is to get SNR, CNR, FBER and Entropy + % This is useful to check coregistration worked fine + anatQA = spmup_anatQA(anatImage, TPMs(1, :), TPMs(2, :)); %#ok<*NASGU> - % sanity check that all images are in the same space. - anatImage = fullfile(anatDataDir, anatImage); - volumesToCheck = {anatImage; TPMs(1, :); TPMs(2, :)}; - spm_check_orientations(spm_vol(char(volumesToCheck))); + anatQA.avgDistToSurf = spmup_comp_dist2surf(anatImage); - % Basic QA for anatomical data is to get SNR, CNR, FBER and Entropy - % This is useful to check coregistration worked fine - anatQA = spmup_anatQA(anatImage, TPMs(1, :), TPMs(2, :)); %#ok<*NASGU> + spm_jsonwrite( ... + strrep(anatImage, '.nii', '_qa.json'), ... + anatQA, ... + struct('indent', ' ')); - anatQA.avgDistToSurf = spmup_comp_dist2surf(anatImage); - - spm_jsonwrite( ... - strrep(anatImage, '.nii', '_qa.json'), ... - anatQA, ... - struct('indent', ' ')); - - end end end diff --git a/src/QA/functionalQA.m b/src/QA/functionalQA.m index f804344b..fa356499 100644 --- a/src/QA/functionalQA.m +++ b/src/QA/functionalQA.m @@ -35,106 +35,99 @@ function functionalQA(opt) end opt = loadAndCheckOptions(opt); - [group, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); fprintf(1, ' FUNCTIONAL: QUALITY CONTROL\n\n'); - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) + for iSub = 1:numel(opt.subjects) - groupName = group(iGroup).name; + subID = opt.subjects{iSub}; - for iSub = 1:group(iGroup).numSub + printProcessingSubject(iSub, subID); - subID = group(iGroup).subNumber{iSub}; + % get grey and white matter and csf tissue probability maps + [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); + TPMs = validationInputFile(anatDataDir, anatImage, 'rc[123]'); - printProcessingSubject(groupName, iSub, subID); + % load metrics from anat QA + anatQA = spm_jsonread( ... + fullfile( ... + anatDataDir, ... + strrep(anatImage, '.nii', '_qa.json'))); - % get grey and white matter and csf tissue probability maps - [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); - TPMs = validationInputFile(anatDataDir, anatImage, 'rc[123]'); + [sessions, nbSessions] = getInfo(BIDS, subID, opt, 'Sessions'); - % load metrics from anat QA - anatQA = spm_jsonread( ... - fullfile( ... - anatDataDir, ... - strrep(anatImage, '.nii', '_qa.json'))); + for iSes = 1:nbSessions - [sessions, nbSessions] = getInfo(BIDS, subID, opt, 'Sessions'); + % get all runs for that subject across all sessions + [runs, nbRuns] = getInfo(BIDS, subID, opt, 'Runs', sessions{iSes}); - for iSes = 1:nbSessions + for iRun = 1:nbRuns - % get all runs for that subject across all sessions - [runs, nbRuns] = getInfo(BIDS, subID, opt, 'Runs', sessions{iSes}); + % get the filename for this bold run for this task + [fileName, subFuncDataDir] = getBoldFilename( ... + BIDS, ... + subID, ... + sessions{iSes}, ... + runs{iRun}, ... + opt); - for iRun = 1:nbRuns + prefix = getPrefix('funcQA', opt); + funcImage = validationInputFile(subFuncDataDir, fileName, prefix); - % get the filename for this bold run for this task - [fileName, subFuncDataDir] = getBoldFilename( ... - BIDS, ... - subID, ... - sessions{iSes}, ... - runs{iRun}, ... - opt); + % sanity check that all images are in the same space. + volumesToCheck = {funcImage; TPMs(1, :); TPMs(2, :); TPMs(3, :)}; + spm_check_orientations(spm_vol(char(volumesToCheck))); - prefix = getPrefix('funcQA', opt); - funcImage = validationInputFile(subFuncDataDir, fileName, prefix); + fMRIQA = computeFuncQAMetrics(funcImage, TPMs, anatQA.avgDistToSurf, opt); - % sanity check that all images are in the same space. - volumesToCheck = {funcImage; TPMs(1, :); TPMs(2, :); TPMs(3, :)}; - spm_check_orientations(spm_vol(char(volumesToCheck))); + % TODO + % find an ouput format that is leaner than a 3 Gb json file!!! + % spm_jsonwrite( ... + % fullfile( ... + % subFuncDataDir, ... + % strrep(fileName, '.nii', '_qa.json')), ... + % fMRIQA, ... + % struct('indent', ' ')); + % save( ... + % fullfile( ... + % subFuncDataDir, ... + % strrep(fileName, '.nii', '_qa.mat')), ... + % 'fMRIQA'); - fMRIQA = computeFuncQAMetrics(funcImage, TPMs, anatQA.avgDistToSurf, opt); + outputFiles = spmup_first_level_qa( ... + funcImage, ... + 'Voltera', 'on', ... + 'Radius', anatQA.avgDistToSurf); - % TODO - % find an ouput format that is leaner than a 3 Gb json file!!! - % spm_jsonwrite( ... - % fullfile( ... - % subFuncDataDir, ... - % strrep(fileName, '.nii', '_qa.json')), ... - % fMRIQA, ... - % struct('indent', ' ')); - % save( ... - % fullfile( ... - % subFuncDataDir, ... - % strrep(fileName, '.nii', '_qa.mat')), ... - % 'fMRIQA'); + movefile( ... + fullfile(subFuncDataDir, 'spmup_QC.ps'), ... + fullfile(subFuncDataDir, strrep(fileName, '.nii', '_qa.ps'))); - outputFiles = spmup_first_level_qa( ... - funcImage, ... - 'Voltera', 'on', ... - 'Radius', anatQA.avgDistToSurf); + confounds = load(outputFiles.design); - movefile( ... - fullfile(subFuncDataDir, 'spmup_QC.ps'), ... - fullfile(subFuncDataDir, strrep(fileName, '.nii', '_qa.ps'))); + spm_save( ... + fullfile( ... + subFuncDataDir, ... + strrep(fileName, ... + '_bold.nii', ... + '_desc-confounds_regressors.tsv')), ... + confounds); - confounds = load(outputFiles.design); + delete(outputFiles.design); - spm_save( ... - fullfile( ... - subFuncDataDir, ... - strrep(fileName, ... - '_bold.nii', ... - '_desc-confounds_regressors.tsv')), ... - confounds); + createDataDictionary(subFuncDataDir, fileName, size(confounds, 2)); - delete(outputFiles.design); - - createDataDictionary(subFuncDataDir, fileName, size(confounds, 2)); - - % create carpet plot - - % horrible hack to prevent the "abrupt" way spmup_volumecorr crashes - % if nansum is not there - if exist('nansum', 'file') == 2 - spmup_timeseriesplot(funcImage, TPMs(1, :), TPMs(2, :), TPMs(3, :), ... - 'motion', 'on', ... - 'nuisances', 'on', ... - 'correlation', 'on', ... - 'makefig', 'on'); - end + % create carpet plot + % horrible hack to prevent the "abrupt" way spmup_volumecorr crashes + % if nansum is not there + if exist('nansum', 'file') == 2 + spmup_timeseriesplot(funcImage, TPMs(1, :), TPMs(2, :), TPMs(3, :), ... + 'motion', 'on', ... + 'nuisances', 'on', ... + 'correlation', 'on', ... + 'makefig', 'on'); end end diff --git a/src/batches/setBatchSTC.m b/src/batches/setBatchSTC.m index 63595245..0d4d9658 100644 --- a/src/batches/setBatchSTC.m +++ b/src/batches/setBatchSTC.m @@ -52,7 +52,7 @@ % round acquisition time to the upper millisecond % mostly to avoid having errors when checking: % any(sliceOrder > TA) - TA = ceil(TA * 1000)/1000; + TA = ceil(TA * 1000) / 1000; maxSliceTime = max(sliceOrder); minSliceTime = min(sliceOrder); From 79c66d417d55204d43b5d8e265d69ab5acd27ba3 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Tue, 9 Mar 2021 13:20:54 +0100 Subject: [PATCH 017/145] fix bug moving figures in bidsFFX --- src/workflows/bidsFFX.m | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/workflows/bidsFFX.m b/src/workflows/bidsFFX.m index 34b2722e..e6e75db6 100644 --- a/src/workflows/bidsFFX.m +++ b/src/workflows/bidsFFX.m @@ -75,8 +75,6 @@ function bidsFFX(action, opt, funcFWHM) deleteResidualImages(getFFXdir(subLabel, funcFWHM, opt)); - movefile(['sub-', subLabel, '_task-', opt.taskName, '_design_*'], ... - getFFXdir(subLabel, funcFWHM, opt)); end case 'contrasts' From 40faeb6e7882da9381344840b4de37be058d4c10 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Tue, 9 Mar 2021 13:49:35 +0100 Subject: [PATCH 018/145] add initCppSpm to face rep demo --- demos/spm_face_rep/face_rep_run.m | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/demos/spm_face_rep/face_rep_run.m b/demos/spm_face_rep/face_rep_run.m index 5ea3695e..23a1885f 100644 --- a/demos/spm_face_rep/face_rep_run.m +++ b/demos/spm_face_rep/face_rep_run.m @@ -26,9 +26,7 @@ % directory with this script becomes the current directory WD = fileparts(mfilename('fullpath')); -% we add all the subfunctions that are in the sub directories -addpath(genpath(fullfile(WD, '..', '..', 'src'))); -addpath(genpath(fullfile(WD, '..', '..', 'lib'))); +initCppSpm(); %% Set options opt = FaceRep_getOption(); From e7647408e970fea32050b842b22cc56cefab0271 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Tue, 9 Mar 2021 14:29:30 +0100 Subject: [PATCH 019/145] fix bugs in ds000001 demo --- demos/openneuro/ds000001_run.m | 7 +------ src/workflows/bidsRFX.m | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/demos/openneuro/ds000001_run.m b/demos/openneuro/ds000001_run.m index 38d9a18e..75bf98a4 100644 --- a/demos/openneuro/ds000001_run.m +++ b/demos/openneuro/ds000001_run.m @@ -9,12 +9,7 @@ FWHM = 6; conFWHM = 6; -% directory with this script becomes the current directory -WD = fileparts(mfilename('fullpath')); - -% we add all the subfunctions that are in the sub directories -addpath(genpath(fullfile(WD, '..', '..', 'src'))); -addpath(genpath(fullfile(WD, '..', '..', 'lib'))); +initCppSpm(); %% Set options opt = ds000001_getOption(); diff --git a/src/workflows/bidsRFX.m b/src/workflows/bidsRFX.m index f86a26e7..f7a66606 100644 --- a/src/workflows/bidsRFX.m +++ b/src/workflows/bidsRFX.m @@ -82,7 +82,7 @@ function bidsRFX(action, opt, funcFWHM, conFWHM) % Load the list of contrasts of interest for the RFX grpLvlCon = getGrpLevelContrastToCompute(opt); - matlabbatch = setBatchEstimateModel(matlabbatch, grpLvlCon, opt); + matlabbatch = setBatchEstimateModel(matlabbatch, opt, grpLvlCon); saveAndRunWorkflow(matlabbatch, 'group_level_model_specification_estimation', opt); From 7b145126af7e9a6d34501873001bc0dbe8c3c12b Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Tue, 9 Mar 2021 14:36:55 +0100 Subject: [PATCH 020/145] add initCppSpm to ds000114 --- demos/openneuro/ds000114_run.m | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/demos/openneuro/ds000114_run.m b/demos/openneuro/ds000114_run.m index 0c72597d..ad3c9366 100644 --- a/demos/openneuro/ds000114_run.m +++ b/demos/openneuro/ds000114_run.m @@ -9,12 +9,7 @@ FWHM = 6; conFWHM = 6; -% directory with this script becomes the current directory -WD = fileparts(mfilename('fullpath')); - -% we add all the subfunctions that are in the sub directories -addpath(genpath(fullfile(WD, '..', '..', 'src'))); -addpath(genpath(fullfile(WD, '..', '..', 'lib'))); +initCppSpm(); %% Set options opt = ds000114_getOption(); From 7eb426269e73175c2f977f50970abbedd0ceac49 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Tue, 9 Mar 2021 14:42:01 +0100 Subject: [PATCH 021/145] update github actions --- .github/workflows/run_system_tests.yml | 3 +-- .github/workflows/run_tests.yml | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/run_system_tests.yml b/.github/workflows/run_system_tests.yml index 88c96be0..9ae64cf0 100644 --- a/.github/workflows/run_system_tests.yml +++ b/.github/workflows/run_system_tests.yml @@ -58,8 +58,7 @@ jobs: - name: Update octave path run: | - octave $OCTFLAGS --eval "addpath(genpath(fullfile(pwd, 'lib'))); savepath();" - octave $OCTFLAGS --eval "addpath(genpath(fullfile(pwd, 'src'))); savepath();" + octave $OCTFLAGS --eval "initCppSpm; savepath();" - name: Prepare data run: | diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index f45c7522..6fbd99b0 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -62,8 +62,8 @@ jobs: - name: Update octave path run: | - octave $OCTFLAGS --eval "addpath(genpath(fullfile(pwd, 'lib'))); savepath();" - octave $OCTFLAGS --eval "addpath(genpath(fullfile(pwd, 'src'))); savepath();" + octave $OCTFLAGS --eval "initCppSpm; savepath();" + octave $OCTFLAGS --eval "addpath(fullfile(pwd, 'tests', 'utils')); savepath();" - name: Prepare data run: | From e70eeeb088000ebc8a228bb1f21d8921b5d1483c Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Tue, 9 Mar 2021 15:04:02 +0100 Subject: [PATCH 022/145] fix issues with demo ds001168 --- demos/openneuro/ds001168_run.m | 7 +------ src/workflows/bidsCopyRawFolder.m | 2 +- src/workflows/bidsCreateVDM.m | 4 +--- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/demos/openneuro/ds001168_run.m b/demos/openneuro/ds001168_run.m index e8ab780c..26f3b406 100644 --- a/demos/openneuro/ds001168_run.m +++ b/demos/openneuro/ds001168_run.m @@ -8,12 +8,7 @@ % Smoothing to apply FWHM = 6; -% directory with this script becomes the current directory -WD = fileparts(mfilename('fullpath')); - -% we add all the subfunctions that are in the sub directories -addpath(genpath(fullfile(WD, '..', '..', 'src'))); -addpath(genpath(fullfile(WD, '..', '..', 'lib'))); +initCppSpm(); %% Set options opt = ds001168_getOption(); diff --git a/src/workflows/bidsCopyRawFolder.m b/src/workflows/bidsCopyRawFolder.m index de692872..e647e77e 100644 --- a/src/workflows/bidsCopyRawFolder.m +++ b/src/workflows/bidsCopyRawFolder.m @@ -249,7 +249,7 @@ function unzipFiles(derivativesDir, deleteZippedNii, opt) % for bold, physio and stim files, we only unzip the files of the task of % interest - if any(strcmp(fragments.type, {'bold', 'stim', 'physio'})) && ... + if any(strcmp(fragments.type, {'bold', 'stim'})) && ... isfield(fragments, 'task') && strcmp(fragments.task, opt.taskName) % load the nifti image and saves the functional data as unzipped nii diff --git a/src/workflows/bidsCreateVDM.m b/src/workflows/bidsCreateVDM.m index f3a83ec6..baf622a1 100644 --- a/src/workflows/bidsCreateVDM.m +++ b/src/workflows/bidsCreateVDM.m @@ -31,14 +31,12 @@ function bidsCreateVDM(opt) subLabel = opt.subjects{iSub}; - printProcessingSubject(iSub, subLabel); - % TODO Move to getInfo types = bids.query(BIDS, 'types', 'sub', subLabel); if any(ismember(types, {'phase12', 'phasediff', 'fieldmap', 'epi'})) - printProcessingSubject(groupName, iSub, subLabel); + printProcessingSubject(iSub, subLabel); % Create rough mean of the 1rst run to improve SNR for coregistration % TODO use the slice timed EPI if STC was used ? From c668cd090d05b8757b6b8dd1f531cfc3fe729ec5 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 10 Mar 2021 11:17:14 +0100 Subject: [PATCH 023/145] fix test --- tests/test_unit_getFFXdir.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_unit_getFFXdir.m b/tests/test_unit_getFFXdir.m index 7eb67e59..09fecf07 100644 --- a/tests/test_unit_getFFXdir.m +++ b/tests/test_unit_getFFXdir.m @@ -1,6 +1,6 @@ % (C) Copyright 2020 CPP BIDS SPM-pipeline developers -function test_suite = test_getFFXdir %#ok<*STOUT> +function test_suite = test_unit_getFFXdir %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> catch % no problem; early Matlab versions can use initTestSuite fine @@ -13,7 +13,7 @@ function test_getFFXdirBasic() funcFWFM = 0; subLabel = '01'; - opt = setOptions('vislocalizer', subLabel); + opt = setOptions('funcLocalizer', subLabel); opt = setDerivativesDir(opt); opt = checkOptions(opt); From a92a4803e9dbe867dc484aab9ae63f73dc184c91 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 10 Mar 2021 14:49:14 +0100 Subject: [PATCH 024/145] update test batch STC --- tests/test_setBatchSTC.m | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_setBatchSTC.m b/tests/test_setBatchSTC.m index 39fc736d..93c4941c 100644 --- a/tests/test_setBatchSTC.m +++ b/tests/test_setBatchSTC.m @@ -130,6 +130,7 @@ function test_setBatchSTCErrorInvalidInputTime() nbSlices = length(sliceOrder); TA = TR - (TR / nbSlices); + TA = ceil(TA*1000)/1000; expectedBatch{1}.spm.temporal.st.nslices = nbSlices; expectedBatch{1}.spm.temporal.st.tr = TR; From 910f2d3532c0e092c8bfefbc153e41841b1bb59d Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 8 Mar 2021 15:28:16 +0100 Subject: [PATCH 025/145] use a string for anat session and not an index --- src/defaults/checkOptions.m | 15 +++- src/getAnatFilename.m | 119 +++++++++++++++++++-------- src/unzipImgAndReturnsFullpathName.m | 14 ++-- tests/test_checkOptions.m | 13 ++- tests/test_getAnatFilename.m | 54 +++++++++++- tests/test_loadAndCheckOptions.m | 2 +- 6 files changed, 171 insertions(+), 46 deletions(-) diff --git a/src/defaults/checkOptions.m b/src/defaults/checkOptions.m index 641c65d5..7a19877a 100644 --- a/src/defaults/checkOptions.m +++ b/src/defaults/checkOptions.m @@ -40,7 +40,7 @@ % - ``opt.zeropad = 2`` - number of zeros used for padding subject numbers, in case % subjects should be fetched by their number ``1`` and not their label ``O1'``. % - ``opt.anatReference.type = 'T1w'`` - type of the anatomical reference - % - ``opt.anatReference.session = 1`` - session number of the anatomical reference + % - ``opt.anatReference.session = '01'`` - session label of the anatomical reference % - ``opt.skullstrip.threshold = 0.75`` - Threshold used for the skull stripping. % Any voxel with ``p(grayMatter) + p(whiteMatter) + p(CSF) > threshold`` % will be included in the mask. @@ -85,7 +85,7 @@ fieldsToSet.zeropad = 2; fieldsToSet.anatReference.type = 'T1w'; - fieldsToSet.anatReference.session = []; + fieldsToSet.anatReference.session = ''; %% Options for slice time correction % all in seconds @@ -132,8 +132,6 @@ function checkFields(opt) if ~all(cellfun(@ischar, opt.groups)) - disp(opt.groups); - errorStruct.identifier = 'checkOptions:groupNotString'; errorStruct.message = sprintf( ... 'All group names should be string.'); @@ -141,6 +139,15 @@ function checkFields(opt) end + if ~ischar(opt.anatReference.session) + + errorStruct.identifier = 'checkOptions:sessionNotString'; + errorStruct.message = sprintf( ... + 'The session label should be string.'); + error(errorStruct); + + end + if ~isempty (opt.STC_referenceSlice) && length(opt.STC_referenceSlice) > 1 errorStruct.identifier = 'checkOptions:refSliceNotScalar'; diff --git a/src/getAnatFilename.m b/src/getAnatFilename.m index 03a21add..880f6838 100644 --- a/src/getAnatFilename.m +++ b/src/getAnatFilename.m @@ -2,60 +2,111 @@ function [anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt) % - % Short description of what the function does goes here. + % Get the filename and the directory of an anat file for a given session and run. + % Unzips the file if necessary. % - % USAGE:: + % It several images are available it will take the first one it finds. % - % [argout1, argout2] = templateFunction(argin1, [argin2 == default,] [argin3]) + % USAGE:: % - % :param argin1: (dimension) obligatory argument. Lorem ipsum dolor sit amet, - % consectetur adipiscing elit. Ut congue nec est ac lacinia. - % :type argin1: type - % :param opt: Options chosen for the analysis. See ``checkOptions()``. - % :type opt: structure - % :param argin3: (dimension) optional argument + % [anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt) % - % :returns: - :argout1: (type) (dimension) - % - :argout2: (type) (dimension) + % :param BIDS: + % :type BIDS: structure + % :param subLabel: + % :param subLabel: string + % :type opt: + % :param opt: structure % - % [anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt) + % :returns: - :anatImage: (string) + % - :anatDataDir: (string) % - % Get the filename and the directory of an anat file for a given session / - % run. - % Unzips the file if necessary. - anatType = opt.anatReference.type; + anatSuffix = opt.anatReference.type; + anatSession = opt.anatReference.session; - sessions = getInfo(BIDS, subLabel, opt, 'Sessions'); + checkAvailableSuffix(BIDS, subLabel, anatSuffix); + anatSession = checkAvailableSessions(BIDS, subLabel, opt, anatSession); % get all anat images for that subject fo that type - % TODO allow for the session to be referenced by a string e.g ses-retest anat = bids.query(BIDS, 'data', ... 'sub', subLabel, ... - 'type', anatType); - if ~isempty(opt.anatReference.session) - anatSession = opt.anatReference.session; - anat = bids.query(BIDS, 'data', ... - 'sub', subLabel, ... - 'ses', sessions{anatSession}, ... - 'type', anatType); - end + 'ses', anatSession, ... + 'type', anatSuffix); if isempty(anat) - anat = bids.query(BIDS, 'data', ... - 'sub', subLabel, ... - 'type', anatType); - error('No anat file for the subject %s. Here are all anat file:\n%s', ... - subLabel, ... - char(anat)); + + msgID = 'noAnatFile'; + + msg = sprintf('No anat file for the subject: %s / session: %s/ type: %s.', ... + subLabel, ... + anatSession, ... + anatSuffix); + + getAnatError(msgID, msg); + end % TODO - % We assume that the first anat of that type is the correct one - % could be an issue for dataset with more than one anatomical of the same type + % we take the first image of that suffix/session as the right one. + % it could be required to take another one, or several and mean them... anat = anat{1}; anatImage = unzipImgAndReturnsFullpathName(anat); [anatDataDir, anatImage, ext] = spm_fileparts(anatImage); anatImage = [anatImage ext]; end + +function checkAvailableSuffix(BIDS, subLabel, anatType) + + availableSuffixes = bids.query(BIDS, 'types', ... + 'sub', subLabel); + + if ~strcmp(anatType, availableSuffixes) + + disp(availableSuffixes); + + msgID = 'requestedSuffixUnvailable'; + msg = sprintf(['Requested anatomical suffix %s unavailable for subject %s.'... + ' All available types listed above.'], anatType); + + getAnatError(msgID, msg); + + end + +end + +function anatSession = checkAvailableSessions(BIDS, subLabel, opt, anatSession) + + sessions = getInfo(BIDS, subLabel, opt, 'Sessions'); + + if ~isempty(anatSession) + + if all(~strcmp(anatSession, sessions)) + + disp(sessions); + + msgID = 'requestedSessionUnvailable'; + msg = sprintf(['Requested session %s for anatomical unavailable for subject %s.', ... + ' All available sessions listed above.'], ... + anatSession, ... + subLabel); + + getAnatError(msgID, msg); + + end + + else + anatSession = sessions; + + end + +end + +function getAnatError(msgID, msg) + + errorStruct.identifier = sprintf('getAnatFilename:%s', msgID); + errorStruct.message = msg; + error(errorStruct); + +end diff --git a/src/unzipImgAndReturnsFullpathName.m b/src/unzipImgAndReturnsFullpathName.m index 189b7b01..b8a4d6eb 100644 --- a/src/unzipImgAndReturnsFullpathName.m +++ b/src/unzipImgAndReturnsFullpathName.m @@ -2,17 +2,21 @@ function unzippedFullpathImgName = unzipImgAndReturnsFullpathName(fullpathImgName) % - % Short description of what the function does goes here. + % Unzips an image if necessary % % USAGE:: % % unzippedFullpathImgName = unzipImgAndReturnsFullpathName(fullpathImgName) % - % :param argin1: (dimension) obligatory argument. Lorem ipsum dolor sit amet, - % consectetur adipiscing elit. Ut congue nec est ac lacinia. - % :type argin1: type + % :param fullpathImgName: + % :type fullpathImgName: string + % + % :returns: - :unzippedFullpathImgName: (string) + % + % TODO: + % + % - make it work on several images % - % :returns: - :argout1: (type) (dimension) % [directory, filename, ext] = spm_fileparts(fullpathImgName); diff --git a/tests/test_checkOptions.m b/tests/test_checkOptions.m index 6390bfa0..c8b944b3 100644 --- a/tests/test_checkOptions.m +++ b/tests/test_checkOptions.m @@ -76,6 +76,17 @@ function test_checkOptionsErrorVoxDim() end +function test_checkOptionsSessionString() + + opt.taskName = 'testTask'; + opt.anatReference.session = 1; + + assertExceptionThrown( ... + @()checkOptions(opt), ... + 'checkOptions:sessionNotString'); + +end + function expectedOptions = defaultOptions() expectedOptions.sliceOrder = []; @@ -92,7 +103,7 @@ function test_checkOptionsErrorVoxDim() expectedOptions.space = 'MNI'; expectedOptions.anatReference.type = 'T1w'; - expectedOptions.anatReference.session = []; + expectedOptions.anatReference.session = ''; expectedOptions.skullstrip.threshold = 0.75; diff --git a/tests/test_getAnatFilename.m b/tests/test_getAnatFilename.m index a2e04330..02f0b1bc 100644 --- a/tests/test_getAnatFilename.m +++ b/tests/test_getAnatFilename.m @@ -11,7 +11,7 @@ % TODO % add tests to check: % - errors when the requested file is not in the correct session -% - that the fucntion is smart enough to find an anat even when user has not +% - that the function is smart enough to find an anat even when user has not % specified a session function test_getAnatFilenameBasic() @@ -35,4 +35,56 @@ function test_getAnatFilenameBasic() assertEqual(anatDataDir, expectedAnatDataDir); assertEqual(anatImage, expectedFileName); + %% + opt.anatReference.session = '01'; + opt.anatReference.type = 'T1w'; + + [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); + + assertEqual(anatDataDir, expectedAnatDataDir); + assertEqual(anatImage, expectedFileName); + +end + +function test_getAnatFilenameTypeError() + + subID = '01'; + opt = setOptions(subID); + + opt.anatReference.type = 'T2w'; + + opt = checkOptions(opt); + + [~, opt, BIDS] = getData(opt); + + assertExceptionThrown( ... + @()getAnatFilename(BIDS, subID, opt), ... + 'getAnatFilename:requestedSuffixUnvailable'); + +end + +function test_getAnatFilenameSEssionError() + + subID = '01'; + opt = setOptions(subID); + + opt.anatReference.session = '001'; + + opt = checkOptions(opt); + + [~, opt, BIDS] = getData(opt); + + assertExceptionThrown( ... + @()getAnatFilename(BIDS, subID, opt), ... + 'getAnatFilename:requestedSessionUnvailable'); + +end + +function opt = setOptions(subID) + + opt.taskName = 'vislocalizer'; + opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); + opt.groups = {''}; + opt.subjects = {subID}; + end diff --git a/tests/test_loadAndCheckOptions.m b/tests/test_loadAndCheckOptions.m index 31b09d0e..62ecfe88 100644 --- a/tests/test_loadAndCheckOptions.m +++ b/tests/test_loadAndCheckOptions.m @@ -130,7 +130,7 @@ function test_loadAndCheckOptionsFromSeveralFiles() expectedOptions.space = 'MNI'; expectedOptions.anatReference.type = 'T1w'; - expectedOptions.anatReference.session = []; + expectedOptions.anatReference.session = ''; expectedOptions.skullstrip.threshold = 0.75; From 918a0f5c94b1882539f6aad9ccc4cbadc7370643 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 8 Mar 2021 15:43:07 +0100 Subject: [PATCH 026/145] update default in doc --- src/defaults/checkOptions.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/defaults/checkOptions.m b/src/defaults/checkOptions.m index 7a19877a..421992a8 100644 --- a/src/defaults/checkOptions.m +++ b/src/defaults/checkOptions.m @@ -40,7 +40,7 @@ % - ``opt.zeropad = 2`` - number of zeros used for padding subject numbers, in case % subjects should be fetched by their number ``1`` and not their label ``O1'``. % - ``opt.anatReference.type = 'T1w'`` - type of the anatomical reference - % - ``opt.anatReference.session = '01'`` - session label of the anatomical reference + % - ``opt.anatReference.session = ''`` - session label of the anatomical reference % - ``opt.skullstrip.threshold = 0.75`` - Threshold used for the skull stripping. % Any voxel with ``p(grayMatter) + p(whiteMatter) + p(CSF) > threshold`` % will be included in the mask. From bb183a94b4683ba0bc5d56cd8f040c39198f1aaa Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 10 Mar 2021 15:44:01 +0100 Subject: [PATCH 027/145] mh fix --- src/utils/saveOptions.m | 8 ++++---- tests/test_setBatchSTC.m | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils/saveOptions.m b/src/utils/saveOptions.m index aef5205b..84b2da6f 100644 --- a/src/utils/saveOptions.m +++ b/src/utils/saveOptions.m @@ -13,11 +13,11 @@ function saveOptions(opt) % optionDir = fullfile(pwd, 'cfg'); - [~,~,~] = mkdir(optionDir); + [~, ~, ~] = mkdir(optionDir); filename = fullfile(optionDir, ['options', ... - '_task-', opt.taskName, ... - '_date-' datestr(now, 'yyyymmddHHMM'), ... - '.json']); + '_task-', opt.taskName, ... + '_date-' datestr(now, 'yyyymmddHHMM'), ... + '.json']); jsonFormat.indent = ' '; spm_jsonwrite(filename, opt, jsonFormat); diff --git a/tests/test_setBatchSTC.m b/tests/test_setBatchSTC.m index 93c4941c..422c8138 100644 --- a/tests/test_setBatchSTC.m +++ b/tests/test_setBatchSTC.m @@ -130,7 +130,7 @@ function test_setBatchSTCErrorInvalidInputTime() nbSlices = length(sliceOrder); TA = TR - (TR / nbSlices); - TA = ceil(TA*1000)/1000; + TA = ceil(TA * 1000) / 1000; expectedBatch{1}.spm.temporal.st.nslices = nbSlices; expectedBatch{1}.spm.temporal.st.tr = TR; From f892f704bb4c70fd8aba8014887d24c84420bf3e Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 10 Mar 2021 15:58:40 +0100 Subject: [PATCH 028/145] fix test getAnat --- tests/test_getAnatFilename.m | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/tests/test_getAnatFilename.m b/tests/test_getAnatFilename.m index 02f0b1bc..7cebdfd0 100644 --- a/tests/test_getAnatFilename.m +++ b/tests/test_getAnatFilename.m @@ -39,7 +39,7 @@ function test_getAnatFilenameBasic() opt.anatReference.session = '01'; opt.anatReference.type = 'T1w'; - [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); + [anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt); assertEqual(anatDataDir, expectedAnatDataDir); assertEqual(anatImage, expectedFileName); @@ -48,43 +48,36 @@ function test_getAnatFilenameBasic() function test_getAnatFilenameTypeError() - subID = '01'; - opt = setOptions(subID); + subLabel = '01'; + + opt = setOptions('vislocalizer', subLabel); opt.anatReference.type = 'T2w'; opt = checkOptions(opt); - [~, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); assertExceptionThrown( ... - @()getAnatFilename(BIDS, subID, opt), ... + @()getAnatFilename(BIDS, subLabel, opt), ... 'getAnatFilename:requestedSuffixUnvailable'); end function test_getAnatFilenameSEssionError() - subID = '01'; - opt = setOptions(subID); + subLabel = '01'; + + opt = setOptions('vislocalizer', subLabel); opt.anatReference.session = '001'; opt = checkOptions(opt); - [~, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); assertExceptionThrown( ... - @()getAnatFilename(BIDS, subID, opt), ... + @()getAnatFilename(BIDS, subLabel, opt), ... 'getAnatFilename:requestedSessionUnvailable'); end - -function opt = setOptions(subID) - - opt.taskName = 'vislocalizer'; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.groups = {''}; - opt.subjects = {subID}; - -end From 322ab04a6147c2db7a0ccd6987ac7cfc22a6c09a Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 6 Mar 2021 19:44:02 +0100 Subject: [PATCH 029/145] update help sections --- src/getBoldFilename.m | 37 ++++++++++++++++++++----------------- src/getInfo.m | 43 +++++++++++++++++++++++++------------------ 2 files changed, 45 insertions(+), 35 deletions(-) diff --git a/src/getBoldFilename.m b/src/getBoldFilename.m index 6c5f5342..6b2330b2 100644 --- a/src/getBoldFilename.m +++ b/src/getBoldFilename.m @@ -2,30 +2,33 @@ function [boldFileName, subFuncDataDir] = getBoldFilename(varargin) % - % Short description of what the function does goes here. + % Get the filename and the directory of a bold file for a given session / + % run. + % + % Unzips the file if necessary. % % USAGE:: % - % [argout1, argout2] = templateFunction(argin1, [argin2 == default,] [argin3]) + % [boldFileName, subFuncDataDir] = getBoldFilename(BIDS, subID, sessionID, runID, opt) % - % :param argin1: (dimension) obligatory argument. Lorem ipsum dolor sit amet, - % consectetur adipiscing elit. Ut congue nec est ac lacinia. - % :type argin1: type - % :param argin2: optional argument and its default value. And some of the - % options can be shown in litteral like ``this`` or ``that``. - % :type argin2: string - % :param argin3: (dimension) optional argument - % :param opt: Options chosen for the analysis. See ``checkOptions()``. - % :type opt: structure + % :param BIDS: returned by bids.layout when exploring a BIDS data set. + % :type BIDS: structure + % :param subID: label of the subject ; in BIDS lingo that means that for a file name + % ``sub-02_task-foo_bold.nii`` the subID will be the string ``02`` + % :type subID: string + % :param sessionID: session label (for `ses-001`, the label will be `001`) + % :type sessionID: string + % :param runID: run index label (for `run-001`, the label will be `001`) + % :type runID: string + % :param opt: Mostly used to find the task name. + % :type opt: structure % - % :returns: - :argout1: (type) (dimension) - % - :argout2: (type) (dimension) % - % [fileName, subFuncDataDir] = getBoldFilename(BIDS, opt, subID, sessionID, runID) + % :returns: - :boldFileName: (string) + % - :subFuncDataDir: (string) % - % Get the filename and the directory of a bold file for a given session / - % run. - % Unzips the file if necessary. + % + [BIDS, subID, sessionID, runID, opt] = deal(varargin{:}); diff --git a/src/getInfo.m b/src/getInfo.m index 01bbe415..ab341d5b 100644 --- a/src/getInfo.m +++ b/src/getInfo.m @@ -9,30 +9,37 @@ % % varargout = getInfo(BIDS, subLabel, opt, info, varargin) % - % :param BIDS: (structure) returned by bids.query when exploring a BIDS data set. - % :param subLabel: ID of the subject - % :param opt: (structure) Mostly used to find the task name. - % :param info: (strint) ``sessions``, ``runs``, ``filename``. - % :param varargin: see below + % If info = ``sessions`, this returns name of the sessions and their number:: % - % - subLabel - ID of the subject ; in BIDS lingo that means that for a file name - % ``sub-02_task-foo_bold.nii`` the subLabel will be the string ``02`` - % - session - ID of the session of interes ; in BIDS lingo that means that for a file name - % ``sub-02_ses-pretest_task-foo_bold.nii`` the sesssion will be the string - % ``pretest`` - % - run: ID of the run of interest - % - type - string ; modality type to look for. For example: ``bold``, ``events``, - % ``stim``, ``physio`` + % [sessions, nbSessions] = getInfo(BIDS, subID, opt, 'sessions') % - % for a given BIDS data set, subject identity, and info type, + % If info = ``runs``, this returns name of the runs and their number for a + % specified session:: % - % if info = Sessions, this returns name of the sessions and their number + % [runs, nbRuns] = getInfo(BIDS, subLabel, opt, 'runs', sessionID) % - % if info = Runs, this returns name of the runs and their number for an specified session. + % If info = ``filename``, this returns the name of the file for a specified + % session and run:: % - % if info = Filename, this returns the name of the file for an specified - % session and run. + % filenames = getInfo(BIDS, subLabel, opt, 'filename', sessionID, runID, type) % + % :param BIDS: returned by bids.layout when exploring a BIDS data set. + % :type BIDS: structure + % :param subLabel: label of the subject ; in BIDS lingo that means that for a file name + % ``sub-02_task-foo_bold.nii`` the subLabel will be the string ``02`` + % :type subLabel: string + % :param opt: Mostly used to find the task name. + % :type opt: structure + % :param info: ``sessions``, ``runs``, ``filename``. + % :type info: string + % :param sessionLabel: session label (for `ses-001`, the label will be `001`) + % :type sessionLabel: string + % :param runIdx: run index label (for `run-001`, the label will be `001`) + % :type runIdx: string + % :param type: datatype (``bold``, ``events``, ``physio``) + % :type type: string + % + varargout = {}; %#ok<*NASGU> From 76aefab5ee634e21b9cc116ff4c585e8de000bd2 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 6 Mar 2021 20:24:51 +0100 Subject: [PATCH 030/145] refactor getInfo --- src/getInfo.m | 52 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/src/getInfo.m b/src/getInfo.m index ab341d5b..5301ad2e 100644 --- a/src/getInfo.m +++ b/src/getInfo.m @@ -23,19 +23,27 @@ % % filenames = getInfo(BIDS, subLabel, opt, 'filename', sessionID, runID, type) % + % % :param BIDS: returned by bids.layout when exploring a BIDS data set. % :type BIDS: structure + % % :param subLabel: label of the subject ; in BIDS lingo that means that for a file name - % ``sub-02_task-foo_bold.nii`` the subLabel will be the string ``02`` + % ``sub-02_task-foo_bold.nii`` the subID will be the string ``02`` % :type subLabel: string - % :param opt: Mostly used to find the task name. - % :type opt: structure + % + % :param opt: Used to find the task name and to pass extra ``query`` + % options. + % :type opt: structure + % % :param info: ``sessions``, ``runs``, ``filename``. % :type info: string + % % :param sessionLabel: session label (for `ses-001`, the label will be `001`) % :type sessionLabel: string + % % :param runIdx: run index label (for `run-001`, the label will be `001`) % :type runIdx: string + % % :param type: datatype (``bold``, ``events``, ``physio``) % :type type: string % @@ -46,10 +54,13 @@ switch lower(info) case 'sessions' + + query = struct(... + 'sub', subLabel, ... + 'task', opt.taskName); - sessions = bids.query(BIDS, 'sessions', ... - 'sub', subLabel, ... - 'task', opt.taskName); + sessions = bids.query(BIDS, 'sessions', query); + nbSessions = size(sessions, 2); if nbSessions == 0 nbSessions = 1; @@ -61,12 +72,15 @@ case 'runs' session = varargin{1}; + + query = struct(... + 'sub', subLabel, ... + 'task', opt.taskName, ... + 'ses', session, ... + 'type', 'bold'); + + runs = bids.query(BIDS, 'runs', query); - runs = bids.query(BIDS, 'runs', ... - 'sub', subLabel, ... - 'task', opt.taskName, ... - 'ses', session, ... - 'type', 'bold'); nbRuns = size(runs, 2); % Get the number of runs if nbRuns == 0 @@ -79,13 +93,15 @@ case 'filename' [session, run, type] = deal(varargin{:}); - - varargout = bids.query(BIDS, 'data', ... - 'sub', subLabel, ... - 'run', run, ... - 'ses', session, ... - 'task', opt.taskName, ... - 'type', type); + + query = struct(... + 'sub', subLabel, ... + 'task', opt.taskName, ... + 'ses', session, ... + 'run', run, ... + 'type', type); + + varargout = bids.query(BIDS, 'data', query); otherwise error('Not sure what info you want me to get.'); From b06e57f66cb1002ce11c5b790c33c4f70cb34360 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 6 Mar 2021 21:17:06 +0100 Subject: [PATCH 031/145] update getInfo to allow to filter query for extra information --- src/defaults/checkOptions.m | 45 +++---- src/getInfo.m | 7 +- src/utils/setDefaultFields.m | 23 ++-- tests/createDummyDataSet.sh | 3 + tests/test_checkOptions.m | 43 ------- tests/test_getBoldFilename.m | 2 + tests/test_getInfo.m | 110 ++++++++++++++++++ tests/test_loadAndCheckOptions.m | 45 +------ tests/test_setBatchSTC.m | 3 +- tests/test_setBatchSaveCoregistrationMatrix.m | 1 + tests/test_unit_createDataDictionary.m | 2 + tests/utils/defaultOptions.m | 42 +++++++ 12 files changed, 203 insertions(+), 123 deletions(-) create mode 100644 tests/test_getInfo.m create mode 100644 tests/utils/defaultOptions.m diff --git a/src/defaults/checkOptions.m b/src/defaults/checkOptions.m index 421992a8..e622e415 100644 --- a/src/defaults/checkOptions.m +++ b/src/defaults/checkOptions.m @@ -37,27 +37,30 @@ % model to speficy and the contrasts to compute. % % OTHER OPTIONS (with their defaults): - % - ``opt.zeropad = 2`` - number of zeros used for padding subject numbers, in case - % subjects should be fetched by their number ``1`` and not their label ``O1'``. - % - ``opt.anatReference.type = 'T1w'`` - type of the anatomical reference - % - ``opt.anatReference.session = ''`` - session label of the anatomical reference - % - ``opt.skullstrip.threshold = 0.75`` - Threshold used for the skull stripping. - % Any voxel with ``p(grayMatter) + p(whiteMatter) + p(CSF) > threshold`` - % will be included in the mask. - % - ``opt.funcVoxelDims = []`` - Voxel dimensions to use for resampling of functional data - % at normalization. - % - ``opt.STC_referenceSlice = []`` - reference slice for the slice timing correction. - % If left emtpy the mid-volume acquisition time point will be selected at run time. - % - ``opt.sliceOrder = []`` - To be used if SPM can't extract slice info. NOT RECOMMENDED: - % if you know the order in which slices were acquired, you should be able to recompute - % slice timing and add it to the json files in your BIDS data set. + % - ``opt.zeropad = 2`` - number of zeros used for padding subject numbers, in case + % subjects should be fetched by their number ``1`` and not their label ``O1'``. + % - ``opt.query`` - a cell string used to specify other options to only run analysis on + % certain files. ``{'dir', 'AP', 'acq' '3p00mm'}``. See ``bids.query`` + % to see how to specify. + % - ``opt.anatReference.type = 'T1w'`` - type of the anatomical reference + % - ``opt.anatReference.session = ''`` - session label of the anatomical reference + % - ``opt.skullstrip.threshold = 0.75`` - Threshold used for the skull stripping. + % Any voxel with ``p(grayMatter) + p(whiteMatter) + p(CSF) > threshold`` + % will be included in the mask. + % - ``opt.funcVoxelDims = []`` - Voxel dimensions to use for resampling of functional data + % at normalization. + % - ``opt.STC_referenceSlice = []`` - reference slice for the slice timing correction. + % If left emtpy the mid-volume acquisition time point will be selected at run time. + % - ``opt.sliceOrder = []`` - To be used if SPM can't extract slice info. NOT RECOMMENDED: + % if you know the order in which slices were acquired, you should be able to recompute + % slice timing and add it to the json files in your BIDS data set. % - ``opt.glmQA.do = true;`` - If set to ``true```the residual images of a - % GLM at the subject levels will be used to estimate if there is any remaining structure - % in the GLM residuals (the power spectra are not flat) that could indicate - % the subject level results are likely confounded (see - % ``plot_power_spectra_of_GLM_residuals``) and 'Accurate autocorrelation modeling - % substantially improves fMRI reliability' - % _https://www.nature.com/articles/s41467-019-09230-w.pdf + % GLM at the subject levels will be used to estimate if there is any remaining structure + % in the GLM residuals (the power spectra are not flat) that could indicate + % the subject level results are likely confounded (see + % ``plot_power_spectra_of_GLM_residuals``) and 'Accurate autocorrelation modeling + % substantially improves fMRI reliability' + % _https://www.nature.com/articles/s41467-019-09230-w.pdf % fieldsToSet = setDefaultOption(); @@ -83,6 +86,8 @@ fieldsToSet.groups = {''}; fieldsToSet.subjects = {[]}; fieldsToSet.zeropad = 2; + + fieldsToSet.query = struct([]); fieldsToSet.anatReference.type = 'T1w'; fieldsToSet.anatReference.session = ''; diff --git a/src/getInfo.m b/src/getInfo.m index 5301ad2e..39eab4a0 100644 --- a/src/getInfo.m +++ b/src/getInfo.m @@ -100,9 +100,14 @@ 'ses', session, ... 'run', run, ... 'type', type); + + % use the extra query options specified in the options + query = setDefaultFields(query, opt.query); - varargout = bids.query(BIDS, 'data', query); + filenames = bids.query(BIDS, 'data', query); + varargout = {char(filenames)}; + otherwise error('Not sure what info you want me to get.'); diff --git a/src/utils/setDefaultFields.m b/src/utils/setDefaultFields.m index 3f514ddb..f7bde6cd 100644 --- a/src/utils/setDefaultFields.m +++ b/src/utils/setDefaultFields.m @@ -2,28 +2,23 @@ function structure = setDefaultFields(structure, fieldsToSet) % - % Short description of what the function does goes here. + % recursively loop through the fields of a structure and sets a value if they don't exist % % USAGE:: % - % [argout1, argout2] = templateFunction(argin1, [argin2 == default,] [argin3]) - % - % :param argin1: (dimension) obligatory argument. Lorem ipsum dolor sit amet, - % consectetur adipiscing elit. Ut congue nec est ac lacinia. - % :type argin1: type - % :param argin2: optional argument and its default value. And some of the - % options can be shown in litteral like ``this`` or ``that``. - % :type argin2: string - % :param argin3: (dimension) optional argument + % structure = setDefaultFields(structure, fieldsToSet) % - % :returns: - :argout1: (type) (dimension) - % - :argout2: (type) (dimension) + % :param structure: + % :type structure: + % :param fieldsToSet: + % :type fieldsToSet: string % - % structure = setDefaultFields(structure, fieldsToSet) + % :returns: - :structure: (structure) % - % recursively loop through the fields of a structure and sets a value if they don't exist + % % + if isempty(fieldsToSet) return end diff --git a/tests/createDummyDataSet.sh b/tests/createDummyDataSet.sh index 89d39117..9cea3ea2 100755 --- a/tests/createDummyDataSet.sh +++ b/tests/createDummyDataSet.sh @@ -32,6 +32,9 @@ do touch $ThisDir/asub-$Subject\_ses-$Ses\_task-vismotion_run-1_bold.nii touch $ThisDir/asub-$Subject\_ses-$Ses\_task-vismotion_run-2_bold.nii + touch $ThisDir/sub-$Subject\_ses-$Ses\_task-vismotion_acq-1p60mm_run-1_bold.nii + touch $ThisDir/sub-$Subject\_ses-$Ses\_task-vismotion_acq-1p60mm_dir-PA_run-1_bold.nii + touch $ThisDir/mean_sub-$Subject\_ses-$Ses\_task-vismotion_run-1_bold.nii touch $ThisDir/sub-$Subject\_ses-$Ses\_task-vislocalizer_bold.nii diff --git a/tests/test_checkOptions.m b/tests/test_checkOptions.m index c8b944b3..a8ae7db1 100644 --- a/tests/test_checkOptions.m +++ b/tests/test_checkOptions.m @@ -86,46 +86,3 @@ function test_checkOptionsSessionString() 'checkOptions:sessionNotString'); end - -function expectedOptions = defaultOptions() - - expectedOptions.sliceOrder = []; - expectedOptions.STC_referenceSlice = []; - - expectedOptions.dataDir = ''; - expectedOptions.derivativesDir = ''; - - expectedOptions.funcVoxelDims = []; - - expectedOptions.groups = {''}; - expectedOptions.subjects = {[]}; - - expectedOptions.space = 'MNI'; - - expectedOptions.anatReference.type = 'T1w'; - expectedOptions.anatReference.session = ''; - - expectedOptions.skullstrip.threshold = 0.75; - - expectedOptions.realign.useUnwarp = true; - expectedOptions.useFieldmaps = true; - - expectedOptions.taskName = ''; - - expectedOptions.zeropad = 2; - - expectedOptions.contrastList = {}; - expectedOptions.model.file = ''; - expectedOptions.model.hrfDerivatives = [0 0]; - - expectedOptions.glmQA.do = true; - - expectedOptions.result.Steps = returnDefaultResultsStructure(); - - expectedOptions.parallelize.do = false; - expectedOptions.parallelize.nbWorkers = 3; - expectedOptions.parallelize.killOnExit = true; - - expectedOptions = orderfields(expectedOptions); - -end diff --git a/tests/test_getBoldFilename.m b/tests/test_getBoldFilename.m index cb55235c..9ea0c257 100644 --- a/tests/test_getBoldFilename.m +++ b/tests/test_getBoldFilename.m @@ -21,6 +21,8 @@ function test_getBoldFilenameBasic() [BIDS, opt] = getData(opt); + opt.query = struct('acq', ''); + sessions = getInfo(BIDS, subLabel, opt, 'Sessions'); runs = getInfo(BIDS, subLabel, opt, 'Runs', sessions{iSes}); diff --git a/tests/test_getInfo.m b/tests/test_getInfo.m new file mode 100644 index 00000000..338e4865 --- /dev/null +++ b/tests/test_getInfo.m @@ -0,0 +1,110 @@ +function test_suite = test_getInfo %#ok<*STOUT> + 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_getInfoBasic() + + % tests for when no session or only one run + opt = setOptions(); + + %% Get sessions from BIDS + opt.taskName = 'vismotion'; + subID = 'ctrl01'; + info = 'sessions'; + + opt = checkOptions(opt); + + [~, opt, BIDS] = getData(opt); + + sessions = getInfo(BIDS, subID, opt, info); + assert(all(strcmp(sessions, {'01' '02'}))); + + %% Get runs from BIDS + session = '01'; + info = 'runs'; + + [~, opt, BIDS] = getData(opt); + + runs = getInfo(BIDS, subID, opt, info, session); + assert(all(strcmp(runs, {'1' '2'}))); + + %% Get runs from BIDS when no run in filename + opt.taskName = 'vislocalizer'; + subID = 'ctrl01'; + session = '01'; + info = 'runs'; + + [~, opt, BIDS] = getData(opt); + + runs = getInfo(BIDS, subID, opt, info, session); + assert(strcmp(runs, {''})); + +end + +function test_getInfoQuery() + + opt = setOptions(); + + %% Get filename from BIDS + opt.taskName = 'vismotion'; + subID = 'ctrl01'; + session = '01'; + run = '1'; + info = 'filename'; + + %% + opt = checkOptions(opt); + + [~, opt, BIDS] = getData(opt); + + filename = getInfo(BIDS, subID, opt, info, session, run, 'bold'); + assertEqual(size(filename, 1), 3); + + %% + opt.query = struct('acq', ''); + + filename = getInfo(BIDS, subID, opt, info, session, run, 'bold'); + FileName = fullfile(fileparts(mfilename('fullpath')), 'dummyData', ... + 'derivatives', 'cpp_spm', ... + ['sub-' subID], ['ses-' session], 'func', ... + ['sub-' subID, ... + '_ses-' session, ... + '_task-' opt.taskName, ... + '_run-' run, ... + '_bold.nii']); + + assert(strcmp(filename, FileName)); + + %% + opt.query = struct('acq', '1p60mm', 'dir', 'PA'); + + filename = getInfo(BIDS, subID, opt, info, session, run, 'bold'); + FileName = fullfile(fileparts(mfilename('fullpath')), 'dummyData', ... + 'derivatives', 'cpp_spm', ... + ['sub-' subID], ['ses-' session], 'func', ... + ['sub-' subID, ... + '_ses-' session, ... + '_task-' opt.taskName, ... + '_acq-' '1p60mm', ... + '_dir-' 'PA', ... + '_run-' run, ... + '_bold.nii']); + + assert(strcmp(filename, FileName)); + +end + +function opt = setOptions() + opt.derivativesDir = fullfile( ... + fileparts(mfilename('fullpath')), ... + 'dummyData', ... + 'derivatives', ... + 'cpp_spm'); + opt.groups = {''}; + opt.subjects = {[], []}; + +end diff --git a/tests/test_loadAndCheckOptions.m b/tests/test_loadAndCheckOptions.m index 62ecfe88..b1128970 100644 --- a/tests/test_loadAndCheckOptions.m +++ b/tests/test_loadAndCheckOptions.m @@ -112,47 +112,4 @@ function test_loadAndCheckOptionsFromSeveralFiles() assertEqual(opt, expectedOptions); -end - -function expectedOptions = defaultOptions() - - expectedOptions.sliceOrder = []; - expectedOptions.STC_referenceSlice = []; - - expectedOptions.dataDir = ''; - expectedOptions.derivativesDir = ''; - - expectedOptions.funcVoxelDims = []; - - expectedOptions.groups = {''}; - expectedOptions.subjects = {[]}; - - expectedOptions.space = 'MNI'; - - expectedOptions.anatReference.type = 'T1w'; - expectedOptions.anatReference.session = ''; - - expectedOptions.skullstrip.threshold = 0.75; - - expectedOptions.realign.useUnwarp = true; - expectedOptions.useFieldmaps = true; - - expectedOptions.taskName = ''; - - expectedOptions.zeropad = 2; - - expectedOptions.contrastList = {}; - expectedOptions.model.file = ''; - expectedOptions.model.hrfDerivatives = [0 0]; - - expectedOptions.glmQA.do = true; - - expectedOptions.result.Steps = returnDefaultResultsStructure(); - - expectedOptions.parallelize.do = false; - expectedOptions.parallelize.nbWorkers = 3; - expectedOptions.parallelize.killOnExit = true; - - expectedOptions = orderfields(expectedOptions); - -end +end \ No newline at end of file diff --git a/tests/test_setBatchSTC.m b/tests/test_setBatchSTC.m index 422c8138..4c63e470 100644 --- a/tests/test_setBatchSTC.m +++ b/tests/test_setBatchSTC.m @@ -91,7 +91,8 @@ function test_setBatchSTCBasic() 'sub', subLabel, ... 'ses', sprintf('0%i', iSes), ... 'task', opt.taskName, ... - 'type', 'bold'); + 'type', 'bold', ... + 'acq', ''); expectedBatch{1}.spm.temporal.st.scans{runCounter} = ... {fileName{1}}; expectedBatch{1}.spm.temporal.st.scans{runCounter + 1} = ... diff --git a/tests/test_setBatchSaveCoregistrationMatrix.m b/tests/test_setBatchSaveCoregistrationMatrix.m index dda848aa..582c346f 100644 --- a/tests/test_setBatchSaveCoregistrationMatrix.m +++ b/tests/test_setBatchSaveCoregistrationMatrix.m @@ -17,6 +17,7 @@ function test_setBatchSaveCoregistrationMatrixBasic() opt = setOptions('vismotion', subLabel); opt = setDerivativesDir(opt); + opt.query = struct('acq', ''); opt = checkOptions(opt); [BIDS, opt] = getData(opt); diff --git a/tests/test_unit_createDataDictionary.m b/tests/test_unit_createDataDictionary.m index 4cc3bdd0..64aebba5 100644 --- a/tests/test_unit_createDataDictionary.m +++ b/tests/test_unit_createDataDictionary.m @@ -20,6 +20,8 @@ function test_createDataDictionaryBasic() [BIDS, opt] = getData(opt); + opt.query = struct('acq', ''); + sessions = getInfo(BIDS, subLabel, opt, 'Sessions'); runs = getInfo(BIDS, subLabel, opt, 'Runs', sessions{iSes}); diff --git a/tests/utils/defaultOptions.m b/tests/utils/defaultOptions.m new file mode 100644 index 00000000..ca239e92 --- /dev/null +++ b/tests/utils/defaultOptions.m @@ -0,0 +1,42 @@ +function expectedOptions = defaultOptions() + + expectedOptions.sliceOrder = []; + expectedOptions.STC_referenceSlice = []; + + expectedOptions.dataDir = ''; + expectedOptions.derivativesDir = ''; + + expectedOptions.funcVoxelDims = []; + + expectedOptions.groups = {''}; + expectedOptions.subjects = {[]}; + + expectedOptions.query = struct([]); + + expectedOptions.space = 'MNI'; + + expectedOptions.anatReference.type = 'T1w'; + expectedOptions.anatReference.session = []; + + expectedOptions.skullstrip.threshold = 0.75; + + expectedOptions.realign.useUnwarp = true; + expectedOptions.useFieldmaps = true; + + expectedOptions.taskName = ''; + + expectedOptions.zeropad = 2; + + expectedOptions.contrastList = {}; + expectedOptions.model.file = ''; + expectedOptions.model.hrfDerivatives = [0 0]; + + expectedOptions.result.Steps = returnDefaultResultsStructure(); + + expectedOptions.parallelize.do = false; + expectedOptions.parallelize.nbWorkers = 3; + expectedOptions.parallelize.killOnExit = true; + + expectedOptions = orderfields(expectedOptions); + +end From d63d8f7912bfd209571f69d17983abbcdfc1a8b7 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 6 Mar 2021 21:23:49 +0100 Subject: [PATCH 032/145] add tests/utils folder to path for tests in CI and update tests README --- tests/README.md | 125 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 118 insertions(+), 7 deletions(-) diff --git a/tests/README.md b/tests/README.md index 28db08aa..dfeb791c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,10 +1,121 @@ -# README +# Tests for CPP_SPM + +We use a series of unit and integration tests to make sure the code behaves as +expected and to also help in development. + +If you are not sure what unit and integration tests are, check the excellent +chapter about that in the +[Turing way](https://the-turing-way.netlify.app/reproducible-research/testing.html). + +## How to run the tests + +### Install MoxUnit + +You need to install +[MOxUnit for matlab and octave](https://github.com/MOxUnit/MOxUnit) to run the +tests. + +Note the install procedure will require you to have +[git](https://git-scm.com/downloads) installed on your computer. If you don't, +you can always download the MoxUnit code with this +[link](https://github.com/MOxUnit/MOxUnit/archive/master.zip). + +Run the following from a terminal in the folder where you want to install +MOxUnit. The `make install` command will find Matlab / Octave on your system and +make sure it plays nice with MoxUnit. + +NOTE: only type in the terminal what is after the `$` sign: + +```bash +# get the code for MOxUnit with git +git clone https://github.com/MOxUnit/MOxUnit.git +# enter the newly created folder and set up MoxUnit +cd MOxUnit +make install +``` + +If you want to check the code coverage on your computer, you can also install +[MOcov for matlab and octave](https://github.com/MOcov/MOcov). Note that this is +also part of the continuous integration of the bids-matlab, so you don't need to +do this. + +To run the tests, make sure the `tests/utils` folder has bee added to the Matlab +/ Octave path + +## Add helper functions to the path + +There are a some help functions you need to add to the Matlab / Octave path to run the tests: + +``` +addpath(fullfile('tests', 'utils')) +``` + +### Install the test data + +You need to run a bash script to create some empty data files: + +From within the `tests` folder. + +``` +sh createDummyDataSet.sh +``` + +### Run the tests + +From the root folder of the bids-matlab folder, you can run the test with one +the following commands. + +```bash +moxunit_runtests tests + +# Or if you want more feedback +moxunit_runtests tests -verbose +``` + +## Adding more tests + +You can use the following function template to write more tests. ```matlab -coverage = mocov( ... - '-expression', 'moxunit_runtests()', ... - '-verbose', ... - '-cover', fullfile(pwd, '..', 'subfun'), .... - '-cover_xml_file', 'coverage.xml', ... - '-cover_html_dir', 'coverage_html') +function test_suite = test_functionToTest() + % This top function is necessary for mox unit to run tests. + % DO NOT CHANGE IT except to adapt the name of the function. + 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_function_to_test_basic() + + %% set up + + + %% data to test against + + + %% test + % assertTrue( ); + % assertFalse( ); + % assertEqual( ); + +end + + +function test_function_to_test_other_usecase() + + %% set up + + + %% data to test against + + + %% test + % assertTrue( ); + % assertFalse( ); + % assertEqual( ); + +end + ``` From b2a38044e10c40c6cfcb99316b62b588b1d172ee Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 6 Mar 2021 21:40:54 +0100 Subject: [PATCH 033/145] update help checkOptions --- src/defaults/checkOptions.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/defaults/checkOptions.m b/src/defaults/checkOptions.m index e622e415..7342fcd2 100644 --- a/src/defaults/checkOptions.m +++ b/src/defaults/checkOptions.m @@ -39,8 +39,8 @@ % OTHER OPTIONS (with their defaults): % - ``opt.zeropad = 2`` - number of zeros used for padding subject numbers, in case % subjects should be fetched by their number ``1`` and not their label ``O1'``. - % - ``opt.query`` - a cell string used to specify other options to only run analysis on - % certain files. ``{'dir', 'AP', 'acq' '3p00mm'}``. See ``bids.query`` + % - ``opt.query`` - a structure used to specify other options to only run analysis on + % certain files. ``struct('dir', 'AP', 'acq' '3p00mm')``. See ``bids.query`` % to see how to specify. % - ``opt.anatReference.type = 'T1w'`` - type of the anatomical reference % - ``opt.anatReference.session = ''`` - session label of the anatomical reference From acb97148a55298e846d2367aa3b7e37fe45e5bab Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 8 Mar 2021 18:05:50 +0100 Subject: [PATCH 034/145] rename the setDefaultFields --- src/batches/setBatchResults.m | 2 +- src/defaults/checkOptions.m | 2 +- src/getInfo.m | 2 +- src/utils/{setDefaultFields.m => setFields.m} | 4 ++-- .../{test_setDefaultFields.m => test_setFields.m} | 14 +++++++------- 5 files changed, 12 insertions(+), 12 deletions(-) rename src/utils/{setDefaultFields.m => setFields.m} (89%) rename tests/{test_setDefaultFields.m => test_setFields.m} (79%) diff --git a/src/batches/setBatchResults.m b/src/batches/setBatchResults.m index 364315a6..fcb6e9f7 100644 --- a/src/batches/setBatchResults.m +++ b/src/batches/setBatchResults.m @@ -23,7 +23,7 @@ % fieldsToSet = returnDefaultResultsStructure(); - result = setDefaultFields(result, fieldsToSet); + result = setFields(result, fieldsToSet); result.Contrasts = replaceEmptyFields(result.Contrasts, fieldsToSet.Contrasts); matlabbatch{end + 1}.spm.stats.results.spmmat = {fullfile(result.dir, 'SPM.mat')}; diff --git a/src/defaults/checkOptions.m b/src/defaults/checkOptions.m index 7342fcd2..8fed594b 100644 --- a/src/defaults/checkOptions.m +++ b/src/defaults/checkOptions.m @@ -65,7 +65,7 @@ fieldsToSet = setDefaultOption(); - opt = setDefaultFields(opt, fieldsToSet); + opt = setFields(opt, fieldsToSet); checkFields(opt); diff --git a/src/getInfo.m b/src/getInfo.m index 39eab4a0..ad77f927 100644 --- a/src/getInfo.m +++ b/src/getInfo.m @@ -102,7 +102,7 @@ 'type', type); % use the extra query options specified in the options - query = setDefaultFields(query, opt.query); + query = setFields(query, opt.query); filenames = bids.query(BIDS, 'data', query); diff --git a/src/utils/setDefaultFields.m b/src/utils/setFields.m similarity index 89% rename from src/utils/setDefaultFields.m rename to src/utils/setFields.m index f7bde6cd..f0c758f0 100644 --- a/src/utils/setDefaultFields.m +++ b/src/utils/setFields.m @@ -1,6 +1,6 @@ % (C) Copyright 2020 CPP BIDS SPM-pipeline developers -function structure = setDefaultFields(structure, fieldsToSet) +function structure = setFields(structure, fieldsToSet) % % recursively loop through the fields of a structure and sets a value if they don't exist % @@ -34,7 +34,7 @@ if isfield(structure(j), names{i}) && isstruct(structure(j).(names{i})) structure(j).(names{i}) = ... - setDefaultFields(structure(j).(names{i}), fieldsToSet.(names{i})); + setFields(structure(j).(names{i}), fieldsToSet.(names{i})); else diff --git a/tests/test_setDefaultFields.m b/tests/test_setFields.m similarity index 79% rename from tests/test_setDefaultFields.m rename to tests/test_setFields.m index 3debd2bb..f449de5a 100644 --- a/tests/test_setDefaultFields.m +++ b/tests/test_setFields.m @@ -1,6 +1,6 @@ % (C) Copyright 2020 CPP BIDS SPM-pipeline developers -function test_suite = test_setDefaultFields %#ok<*STOUT> +function test_suite = test_setFields %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> catch % no problem; early Matlab versions can use initTestSuite fine @@ -8,14 +8,14 @@ initTestSuite; end -function test_setDefaultFieldsWrite() +function test_setFieldsWrite() %% set up structure = struct(); fieldsToSet.field = 1; - structure = setDefaultFields(structure, fieldsToSet); + structure = setFields(structure, fieldsToSet); %% data to test against expectedStructure.field = 1; @@ -25,7 +25,7 @@ function test_setDefaultFieldsWrite() end -function test_setDefaultFieldsNoOverwrite() +function test_setFieldsNoOverwrite() % set up structure.field.subfield_1 = 3; @@ -33,7 +33,7 @@ function test_setDefaultFieldsNoOverwrite() fieldsToSet.field.subfield_1 = 1; fieldsToSet.field.subfield_2 = 1; - structure = setDefaultFields(structure, fieldsToSet); + structure = setFields(structure, fieldsToSet); % data to test against expectedStructure.field.subfield_1 = 3; @@ -44,7 +44,7 @@ function test_setDefaultFieldsNoOverwrite() end -function test_setDefaultFieldsCmplxStruct() +function test_setFieldsCmplxStruct() % set up structure = struct(); @@ -55,7 +55,7 @@ function test_setDefaultFieldsCmplxStruct() fieldsToSet.field.subfield_2(2).name = 'b'; fieldsToSet.field.subfield_2(2).value = 2; - structure = setDefaultFields(structure, fieldsToSet); + structure = setFields(structure, fieldsToSet); % data to test against expectedStructure.field.subfield_1 = 1; From 1ffd06c3406cb19b86772d6fddee81c3a59ffc18 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 8 Mar 2021 18:07:51 +0100 Subject: [PATCH 035/145] update doc and lint --- src/defaults/checkOptions.m | 2 +- src/getBoldFilename.m | 5 ++--- src/getInfo.m | 31 ++++++++++++++++++++++++++----- src/utils/setFields.m | 13 ++++++------- 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/defaults/checkOptions.m b/src/defaults/checkOptions.m index 8fed594b..7c9bcb40 100644 --- a/src/defaults/checkOptions.m +++ b/src/defaults/checkOptions.m @@ -86,7 +86,7 @@ fieldsToSet.groups = {''}; fieldsToSet.subjects = {[]}; fieldsToSet.zeropad = 2; - + fieldsToSet.query = struct([]); fieldsToSet.anatReference.type = 'T1w'; diff --git a/src/getBoldFilename.m b/src/getBoldFilename.m index 6b2330b2..302856b4 100644 --- a/src/getBoldFilename.m +++ b/src/getBoldFilename.m @@ -15,13 +15,13 @@ % :type BIDS: structure % :param subID: label of the subject ; in BIDS lingo that means that for a file name % ``sub-02_task-foo_bold.nii`` the subID will be the string ``02`` - % :type subID: string + % :type subID: string % :param sessionID: session label (for `ses-001`, the label will be `001`) % :type sessionID: string % :param runID: run index label (for `run-001`, the label will be `001`) % :type runID: string % :param opt: Mostly used to find the task name. - % :type opt: structure + % :type opt: structure % % % :returns: - :boldFileName: (string) @@ -29,7 +29,6 @@ % % - [BIDS, subID, sessionID, runID, opt] = deal(varargin{:}); % get the filename for this bold run for this task diff --git a/src/getInfo.m b/src/getInfo.m index ad77f927..8c606992 100644 --- a/src/getInfo.m +++ b/src/getInfo.m @@ -11,7 +11,7 @@ % % If info = ``sessions`, this returns name of the sessions and their number:: % - % [sessions, nbSessions] = getInfo(BIDS, subID, opt, 'sessions') + % [sessions, nbSessions] = getInfo(BIDS, subID, opt, 'sessions') % % If info = ``runs``, this returns name of the runs and their number for a % specified session:: @@ -29,14 +29,18 @@ % % :param subLabel: label of the subject ; in BIDS lingo that means that for a file name % ``sub-02_task-foo_bold.nii`` the subID will be the string ``02`` +<<<<<<< HEAD % :type subLabel: string +======= + % :type subID: string +>>>>>>> update doc and lint % % :param opt: Used to find the task name and to pass extra ``query`` % options. % :type opt: structure % % :param info: ``sessions``, ``runs``, ``filename``. - % :type info: string + % :type info: string % % :param sessionLabel: session label (for `ses-001`, the label will be `001`) % :type sessionLabel: string @@ -48,16 +52,22 @@ % :type type: string % - varargout = {}; %#ok<*NASGU> switch lower(info) case 'sessions' +<<<<<<< HEAD query = struct(... 'sub', subLabel, ... 'task', opt.taskName); +======= + + query = struct( ... + 'sub', subID, ... + 'task', opt.taskName); +>>>>>>> update doc and lint sessions = bids.query(BIDS, 'sessions', query); @@ -72,7 +82,7 @@ case 'runs' session = varargin{1}; - + query = struct(... 'sub', subLabel, ... 'task', opt.taskName, ... @@ -93,6 +103,7 @@ case 'filename' [session, run, type] = deal(varargin{:}); +<<<<<<< HEAD query = struct(... 'sub', subLabel, ... @@ -101,13 +112,23 @@ 'run', run, ... 'type', type); +======= + + query = struct( ... + 'sub', subID, ... + 'task', opt.taskName, ... + 'ses', session, ... + 'run', run, ... + 'type', type); + +>>>>>>> update doc and lint % use the extra query options specified in the options query = setFields(query, opt.query); filenames = bids.query(BIDS, 'data', query); varargout = {char(filenames)}; - + otherwise error('Not sure what info you want me to get.'); diff --git a/src/utils/setFields.m b/src/utils/setFields.m index f0c758f0..76749b74 100644 --- a/src/utils/setFields.m +++ b/src/utils/setFields.m @@ -2,23 +2,22 @@ function structure = setFields(structure, fieldsToSet) % - % recursively loop through the fields of a structure and sets a value if they don't exist + % Recursively loop through the fields of a ``structure`` and sets the values + % as defined in the structure ``fieldsToSet`` if they don't exist. % % USAGE:: % - % structure = setDefaultFields(structure, fieldsToSet) + % structure = setFields(structure, fieldsToSet) % - % :param structure: - % :type structure: - % :param fieldsToSet: + % :param structure: + % :type structure: + % :param fieldsToSet: % :type fieldsToSet: string % % :returns: - :structure: (structure) % - % % - if isempty(fieldsToSet) return end From 0c06d4858e362c733cb63bf152b5d6b4a424b909 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 8 Mar 2021 18:44:30 +0100 Subject: [PATCH 036/145] allow setFields to overwrite the content of a structure --- src/utils/setFields.m | 31 +++++++++++++++++++++++-------- tests/test_setFields.m | 21 +++++++++++++++++++++ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/utils/setFields.m b/src/utils/setFields.m index 76749b74..907dcb7a 100644 --- a/src/utils/setFields.m +++ b/src/utils/setFields.m @@ -1,18 +1,23 @@ % (C) Copyright 2020 CPP BIDS SPM-pipeline developers -function structure = setFields(structure, fieldsToSet) +function structure = setFields(structure, fieldsToSet, overwrite) % - % Recursively loop through the fields of a ``structure`` and sets the values + % Recursively loop through the fields of a target ``structure`` and sets the values % as defined in the structure ``fieldsToSet`` if they don't exist. % + % Content of the target structure can be overwritten by setting the + % ``overwrite```to ``true``. + % % USAGE:: % - % structure = setFields(structure, fieldsToSet) + % structure = setFields(structure, fieldsToSet, overwrite = false) % % :param structure: % :type structure: % :param fieldsToSet: % :type fieldsToSet: string + % :param overwrite: + % :type overwrite: boolean % % :returns: - :structure: (structure) % @@ -22,6 +27,10 @@ return end + if nargin < 3 || isempty(overwrite) + overwrite = false; + end + names = fieldnames(fieldsToSet); for j = 1:numel(structure) @@ -33,14 +42,20 @@ if isfield(structure(j), names{i}) && isstruct(structure(j).(names{i})) structure(j).(names{i}) = ... - setFields(structure(j).(names{i}), fieldsToSet.(names{i})); + setFields(structure(j).(names{i}), fieldsToSet.(names{i}), overwrite); else - structure = setFieldToIfNotPresent( ... - structure, ... - names{i}, ... - thisField); + if ~overwrite + structure = setFieldToIfNotPresent( ... + structure, ... + names{i}, ... + thisField); + else + structure.(names{i}) = thisField; + + end + end end diff --git a/tests/test_setFields.m b/tests/test_setFields.m index f449de5a..dceeb75b 100644 --- a/tests/test_setFields.m +++ b/tests/test_setFields.m @@ -44,6 +44,27 @@ function test_setFieldsNoOverwrite() end +function test_setFieldsOverwrite() + + overwrite = true(); + + % set up + structure.field.subfield_1 = 3; + + fieldsToSet.field.subfield_1 = 1; + fieldsToSet.field.subfield_2 = 1; + + structure = setFields(structure, fieldsToSet, overwrite); + + % data to test against + expectedStructure.field.subfield_1 = 1; + expectedStructure.field.subfield_2 = 1; + + % test + assert(isequal(expectedStructure, structure)); + +end + function test_setFieldsCmplxStruct() % set up From 37b6c3bdc7758bddb7f315ebb5acc82b80b6a3ba Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 8 Mar 2021 20:13:37 +0100 Subject: [PATCH 037/145] allow getInfo to only select a pre-specified session --- src/getInfo.m | 13 ++++++++++++- tests/test_getInfo.m | 23 +++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/getInfo.m b/src/getInfo.m index 8c606992..91c0924a 100644 --- a/src/getInfo.m +++ b/src/getInfo.m @@ -69,6 +69,12 @@ 'task', opt.taskName); >>>>>>> update doc and lint + % upate query with pre-specified options + % overwrite is set to true in this case because we might want to run + % analysis only on certain sessions + overwrite = true; + query = setFields(query, opt.query, overwrite); + sessions = bids.query(BIDS, 'sessions', query); nbSessions = size(sessions, 2); @@ -89,9 +95,11 @@ 'ses', session, ... 'type', 'bold'); + query = setFields(query, opt.query); + runs = bids.query(BIDS, 'runs', query); - nbRuns = size(runs, 2); % Get the number of runs + nbRuns = size(runs, 2); if nbRuns == 0 nbRuns = 1; @@ -121,8 +129,11 @@ 'run', run, ... 'type', type); +<<<<<<< HEAD >>>>>>> update doc and lint % use the extra query options specified in the options +======= +>>>>>>> allow getInfo to only select a pre-specified session query = setFields(query, opt.query); filenames = bids.query(BIDS, 'data', query); diff --git a/tests/test_getInfo.m b/tests/test_getInfo.m index 338e4865..0aaf3264 100644 --- a/tests/test_getInfo.m +++ b/tests/test_getInfo.m @@ -98,6 +98,29 @@ function test_getInfoQuery() end +function test_getInfoQueryWithSessionRestriction() + + opt = setOptions(); + + opt.taskName = 'vismotion'; + + subID = 'ctrl01'; + + opt = checkOptions(opt); + + [~, opt, BIDS] = getData(opt); + + opt.query = struct('ses', {{'01', '02'}}); + [~, nbSessions] = getInfo(BIDS, subID, opt, 'sessions'); + assertEqual(nbSessions, numel(opt.query.ses)); + + opt.query = struct('ses', '02'); + [sessions, nbSessions] = getInfo(BIDS, subID, opt, 'sessions'); + assertEqual(nbSessions, numel(opt.query)); + assertEqual(sessions{1}, opt.query.ses); + +end + function opt = setOptions() opt.derivativesDir = fullfile( ... fileparts(mfilename('fullpath')), ... From 01705e94ce1d2c091b4360220571c09bce2dc0ab Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 8 Mar 2021 22:27:44 +0100 Subject: [PATCH 038/145] fix wrongly resolved merge conflict --- tests/utils/defaultOptions.m | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/utils/defaultOptions.m b/tests/utils/defaultOptions.m index ca239e92..1637ccfe 100644 --- a/tests/utils/defaultOptions.m +++ b/tests/utils/defaultOptions.m @@ -28,6 +28,9 @@ expectedOptions.zeropad = 2; expectedOptions.contrastList = {}; + + expectedOptions.glmQA.do = true; + expectedOptions.model.file = ''; expectedOptions.model.hrfDerivatives = [0 0]; From bce10bedcae61a896757a7e3ed72d00e25ca3287 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 10 Mar 2021 21:13:15 +0100 Subject: [PATCH 039/145] fix test after merge conflict resolution --- src/getInfo.m | 69 +++++----------- tests/test_getInfo.m | 133 ------------------------------- tests/test_loadAndCheckOptions.m | 2 +- tests/test_setBatchSTC.m | 1 + tests/test_unit_getInfo.m | 76 +++++++++++++++--- tests/utils/defaultOptions.m | 6 +- 6 files changed, 91 insertions(+), 196 deletions(-) delete mode 100644 tests/test_getInfo.m diff --git a/src/getInfo.m b/src/getInfo.m index 91c0924a..92cc6bf4 100644 --- a/src/getInfo.m +++ b/src/getInfo.m @@ -24,32 +24,28 @@ % filenames = getInfo(BIDS, subLabel, opt, 'filename', sessionID, runID, type) % % - % :param BIDS: returned by bids.layout when exploring a BIDS data set. - % :type BIDS: structure + % :param BIDS: returned by bids.layout when exploring a BIDS data set. + % :type BIDS: structure % - % :param subLabel: label of the subject ; in BIDS lingo that means that for a file name - % ``sub-02_task-foo_bold.nii`` the subID will be the string ``02`` -<<<<<<< HEAD - % :type subLabel: string -======= - % :type subID: string ->>>>>>> update doc and lint + % :param subLabel: label of the subject ; in BIDS lingo that means that for a file name + % ``sub-02_task-foo_bold.nii`` the subID will be the string ``02`` + % :type subLabel: string % - % :param opt: Used to find the task name and to pass extra ``query`` - % options. - % :type opt: structure + % :param opt: Used to find the task name and to pass extra ``query`` + % options. + % :type opt: structure % - % :param info: ``sessions``, ``runs``, ``filename``. - % :type info: string + % :param info: ``sessions``, ``runs``, ``filename``. + % :type info: string % % :param sessionLabel: session label (for `ses-001`, the label will be `001`) % :type sessionLabel: string % - % :param runIdx: run index label (for `run-001`, the label will be `001`) - % :type runIdx: string + % :param runIdx: run index label (for `run-001`, the label will be `001`) + % :type runIdx: string % - % :param type: datatype (``bold``, ``events``, ``physio``) - % :type type: string + % :param type: datatype (``bold``, ``events``, ``physio``) + % :type type: string % varargout = {}; %#ok<*NASGU> @@ -57,17 +53,10 @@ switch lower(info) case 'sessions' -<<<<<<< HEAD - - query = struct(... - 'sub', subLabel, ... - 'task', opt.taskName); -======= query = struct( ... - 'sub', subID, ... + 'sub', subLabel, ... 'task', opt.taskName); ->>>>>>> update doc and lint % upate query with pre-specified options % overwrite is set to true in this case because we might want to run @@ -76,7 +65,7 @@ query = setFields(query, opt.query, overwrite); sessions = bids.query(BIDS, 'sessions', query); - + nbSessions = size(sessions, 2); if nbSessions == 0 nbSessions = 1; @@ -89,11 +78,11 @@ session = varargin{1}; - query = struct(... - 'sub', subLabel, ... - 'task', opt.taskName, ... - 'ses', session, ... - 'type', 'bold'); + query = struct( ... + 'sub', subLabel, ... + 'task', opt.taskName, ... + 'ses', session, ... + 'type', 'bold'); query = setFields(query, opt.query); @@ -111,29 +100,15 @@ case 'filename' [session, run, type] = deal(varargin{:}); -<<<<<<< HEAD - - query = struct(... - 'sub', subLabel, ... - 'task', opt.taskName, ... - 'ses', session, ... - 'run', run, ... - 'type', type); - -======= query = struct( ... - 'sub', subID, ... + 'sub', subLabel, ... 'task', opt.taskName, ... 'ses', session, ... 'run', run, ... 'type', type); -<<<<<<< HEAD ->>>>>>> update doc and lint % use the extra query options specified in the options -======= ->>>>>>> allow getInfo to only select a pre-specified session query = setFields(query, opt.query); filenames = bids.query(BIDS, 'data', query); diff --git a/tests/test_getInfo.m b/tests/test_getInfo.m deleted file mode 100644 index 0aaf3264..00000000 --- a/tests/test_getInfo.m +++ /dev/null @@ -1,133 +0,0 @@ -function test_suite = test_getInfo %#ok<*STOUT> - 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_getInfoBasic() - - % tests for when no session or only one run - opt = setOptions(); - - %% Get sessions from BIDS - opt.taskName = 'vismotion'; - subID = 'ctrl01'; - info = 'sessions'; - - opt = checkOptions(opt); - - [~, opt, BIDS] = getData(opt); - - sessions = getInfo(BIDS, subID, opt, info); - assert(all(strcmp(sessions, {'01' '02'}))); - - %% Get runs from BIDS - session = '01'; - info = 'runs'; - - [~, opt, BIDS] = getData(opt); - - runs = getInfo(BIDS, subID, opt, info, session); - assert(all(strcmp(runs, {'1' '2'}))); - - %% Get runs from BIDS when no run in filename - opt.taskName = 'vislocalizer'; - subID = 'ctrl01'; - session = '01'; - info = 'runs'; - - [~, opt, BIDS] = getData(opt); - - runs = getInfo(BIDS, subID, opt, info, session); - assert(strcmp(runs, {''})); - -end - -function test_getInfoQuery() - - opt = setOptions(); - - %% Get filename from BIDS - opt.taskName = 'vismotion'; - subID = 'ctrl01'; - session = '01'; - run = '1'; - info = 'filename'; - - %% - opt = checkOptions(opt); - - [~, opt, BIDS] = getData(opt); - - filename = getInfo(BIDS, subID, opt, info, session, run, 'bold'); - assertEqual(size(filename, 1), 3); - - %% - opt.query = struct('acq', ''); - - filename = getInfo(BIDS, subID, opt, info, session, run, 'bold'); - FileName = fullfile(fileparts(mfilename('fullpath')), 'dummyData', ... - 'derivatives', 'cpp_spm', ... - ['sub-' subID], ['ses-' session], 'func', ... - ['sub-' subID, ... - '_ses-' session, ... - '_task-' opt.taskName, ... - '_run-' run, ... - '_bold.nii']); - - assert(strcmp(filename, FileName)); - - %% - opt.query = struct('acq', '1p60mm', 'dir', 'PA'); - - filename = getInfo(BIDS, subID, opt, info, session, run, 'bold'); - FileName = fullfile(fileparts(mfilename('fullpath')), 'dummyData', ... - 'derivatives', 'cpp_spm', ... - ['sub-' subID], ['ses-' session], 'func', ... - ['sub-' subID, ... - '_ses-' session, ... - '_task-' opt.taskName, ... - '_acq-' '1p60mm', ... - '_dir-' 'PA', ... - '_run-' run, ... - '_bold.nii']); - - assert(strcmp(filename, FileName)); - -end - -function test_getInfoQueryWithSessionRestriction() - - opt = setOptions(); - - opt.taskName = 'vismotion'; - - subID = 'ctrl01'; - - opt = checkOptions(opt); - - [~, opt, BIDS] = getData(opt); - - opt.query = struct('ses', {{'01', '02'}}); - [~, nbSessions] = getInfo(BIDS, subID, opt, 'sessions'); - assertEqual(nbSessions, numel(opt.query.ses)); - - opt.query = struct('ses', '02'); - [sessions, nbSessions] = getInfo(BIDS, subID, opt, 'sessions'); - assertEqual(nbSessions, numel(opt.query)); - assertEqual(sessions{1}, opt.query.ses); - -end - -function opt = setOptions() - opt.derivativesDir = fullfile( ... - fileparts(mfilename('fullpath')), ... - 'dummyData', ... - 'derivatives', ... - 'cpp_spm'); - opt.groups = {''}; - opt.subjects = {[], []}; - -end diff --git a/tests/test_loadAndCheckOptions.m b/tests/test_loadAndCheckOptions.m index b1128970..ba2dc50f 100644 --- a/tests/test_loadAndCheckOptions.m +++ b/tests/test_loadAndCheckOptions.m @@ -112,4 +112,4 @@ function test_loadAndCheckOptionsFromSeveralFiles() assertEqual(opt, expectedOptions); -end \ No newline at end of file +end diff --git a/tests/test_setBatchSTC.m b/tests/test_setBatchSTC.m index 4c63e470..28e9ace6 100644 --- a/tests/test_setBatchSTC.m +++ b/tests/test_setBatchSTC.m @@ -69,6 +69,7 @@ function test_setBatchSTCBasic() opt = setOptions('vismotion', subLabel); opt = setDerivativesDir(opt); + opt.query = struct('acq', ''); opt = checkOptions(opt); [BIDS, opt] = getData(opt); diff --git a/tests/test_unit_getInfo.m b/tests/test_unit_getInfo.m index eed1dcfa..8496f3e3 100644 --- a/tests/test_unit_getInfo.m +++ b/tests/test_unit_getInfo.m @@ -1,6 +1,6 @@ % (C) Copyright 2020 CPP BIDS SPM-pipeline developers -function test_suite = test_unit_getInfo %#ok<*STOUT> +function test_suite = test_getInfo %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> catch % no problem; early Matlab versions can use initTestSuite fine @@ -11,33 +11,59 @@ function test_getInfoBasic() subLabel = 'ctrl01'; + opt = setOptions('vismotion', subLabel); - opt = setDerivativesDir(opt); opt = checkOptions(opt); - %% Get sessions from BIDS info = 'sessions'; + opt = checkOptions(opt); + [BIDS, opt] = getData(opt); + sessions = getInfo(BIDS, subLabel, opt, info); assert(all(strcmp(sessions, {'01' '02'}))); %% Get runs from BIDS + session = '01'; info = 'runs'; + [BIDS, opt] = getData(opt); + + runs = getInfo(BIDS, subLabel, opt, info, session); + assert(all(strcmp(runs, {'1' '2'}))); + + %% Get runs from BIDS when no run in filename + opt.taskName = 'vislocalizer'; + subLabel = 'ctrl01'; session = '01'; + info = 'runs'; [BIDS, opt] = getData(opt); + runs = getInfo(BIDS, subLabel, opt, info, session); - assert(all(strcmp(runs, {'1' '2'}))); + assert(strcmp(runs, {''})); - %% Get filename from BIDS - info = 'filename'; +end + +function test_getInfoQuery() + + subLabel = 'ctrl01'; session = '01'; run = '1'; + info = 'filename'; + + opt = setOptions('vismotion', subLabel); + opt = checkOptions(opt); [BIDS, opt] = getData(opt); + + filename = getInfo(BIDS, subLabel, opt, info, session, run, 'bold'); + assertEqual(size(filename, 1), 3); + + opt.query = struct('acq', ''); + filename = getInfo(BIDS, subLabel, opt, info, session, run, 'bold'); FileName = fullfile(fileparts(mfilename('fullpath')), 'dummyData', ... 'derivatives', 'cpp_spm', ... @@ -50,17 +76,41 @@ function test_getInfoBasic() assert(strcmp(filename, FileName)); - %% Get runs from BIDS when no run in filename + %% + opt.query = struct('acq', '1p60mm', 'dir', 'PA'); + + filename = getInfo(BIDS, subLabel, opt, info, session, run, 'bold'); + FileName = fullfile(fileparts(mfilename('fullpath')), 'dummyData', ... + 'derivatives', 'cpp_spm', ... + ['sub-' subLabel], ['ses-' session], 'func', ... + ['sub-' subLabel, ... + '_ses-' session, ... + '_task-' opt.taskName, ... + '_acq-' '1p60mm', ... + '_dir-' 'PA', ... + '_run-' run, ... + '_bold.nii']); + + assert(strcmp(filename, FileName)); + +end + +function test_getInfoQueryWithSessionRestriction() + subLabel = 'ctrl01'; - opt = setOptions('vislocalizer', subLabel); + + opt = setOptions('vismotion', subLabel); opt = checkOptions(opt); - info = 'runs'; + [BIDS, opt] = getData(opt); - session = '01'; + opt.query = struct('ses', {{'01', '02'}}); + [~, nbSessions] = getInfo(BIDS, subLabel, opt, 'sessions'); + assertEqual(nbSessions, numel(opt.query.ses)); - [BIDS, opt] = getData(opt); - runs = getInfo(BIDS, subLabel, opt, info, session); - assert(strcmp(runs, {''})); + opt.query = struct('ses', '02'); + [sessions, nbSessions] = getInfo(BIDS, subLabel, opt, 'sessions'); + assertEqual(nbSessions, numel(opt.query)); + assertEqual(sessions{1}, opt.query.ses); end diff --git a/tests/utils/defaultOptions.m b/tests/utils/defaultOptions.m index 1637ccfe..3337b905 100644 --- a/tests/utils/defaultOptions.m +++ b/tests/utils/defaultOptions.m @@ -1,3 +1,5 @@ +% (C) Copyright 2021 CPP BIDS SPM-pipeline developers + function expectedOptions = defaultOptions() expectedOptions.sliceOrder = []; @@ -28,9 +30,9 @@ expectedOptions.zeropad = 2; expectedOptions.contrastList = {}; - + expectedOptions.glmQA.do = true; - + expectedOptions.model.file = ''; expectedOptions.model.hrfDerivatives = [0 0]; From f22ee28d4db31cae9402544d0b4428a045c27195 Mon Sep 17 00:00:00 2001 From: Michele MacLean Date: Mon, 15 Mar 2021 15:43:49 -0400 Subject: [PATCH 040/145] turn batch into a function --- .../setBatchSegmentationDetectLesion.m | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/src/batches/setBatchSegmentationDetectLesion.m b/src/batches/setBatchSegmentationDetectLesion.m index eca11150..5591bf12 100755 --- a/src/batches/setBatchSegmentationDetectLesion.m +++ b/src/batches/setBatchSegmentationDetectLesion.m @@ -1,16 +1,37 @@ -%----------------------------------------------------------------------- -% Job saved on 08-Mar-2021 11:11:19 by cfg_util (rev $Rev: 7345 $) -% spm SPM - SPM12 (7771) -% cfg_basicio BasicIO - Unknown -%----------------------------------------------------------------------- -matlabbatch{1}.spm.tools.ali.unified_segmentation.step1data = ''; +% (C) Copyright 2021 CPP BIDS SPM-pipeline developers + +function matlabbatch = setBatchSegmentationDetectLesion(matlabbatch, BIDS, opt, subID) + % + % Creates a batch to segment the anatomical image for lesion detection + % + % USAGE:: + % + % matlabbatch = setBatchSegmentationDetectLesion(matlabbatch, BIDS, opt, subID) + % + % :param matlabbatch: list of SPM batches + % :type matlabbatch: structure + % + % :returns: - :matlabbatch: (structure) + + printBatchName('Segmentation for lesion detection'); + +% find anatomical file +[anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); +matlabbatch{1}.spm.tools.ali.unified_segmentation.step1data = [anatImage, anatDataDir]; + +% define SPM folder spmDir = spm('dir'); + +% specify Prior EXTRA class (lesion prior map) lesionPriorMap = fullfile(spmDir, 'toolbox', 'ALI', 'Priors_extraClass', 'wc4prior0.nii'); + matlabbatch{1}.spm.tools.ali.unified_segmentation.step1prior = {lesionPriorMap}; -matlabbatch{1}.spm.tools.ali.unified_segmentation.step1niti = 2; -matlabbatch{1}.spm.tools.ali.unified_segmentation.step1thr_prob = 0.333333333333333; -matlabbatch{1}.spm.tools.ali.unified_segmentation.step1thr_size = 0.8; -matlabbatch{1}.spm.tools.ali.unified_segmentation.step1coregister = 1; -matlabbatch{1}.spm.tools.ali.unified_segmentation.step1mask = {''}; -matlabbatch{1}.spm.tools.ali.unified_segmentation.step1vox = 2; -matlabbatch{1}.spm.tools.ali.unified_segmentation.step1fwhm = [8 8 8]; +matlabbatch{1}.spm.tools.ali.unified_segmentation.step1niti = 2; % number of iterations +matlabbatch{1}.spm.tools.ali.unified_segmentation.step1thr_prob = 0.333333333333333; % threshold probability +matlabbatch{1}.spm.tools.ali.unified_segmentation.step1thr_size = 0.8; % threshold size (in cm3) +matlabbatch{1}.spm.tools.ali.unified_segmentation.step1coregister = 1; % coregister in MNI space (yes: 1) +matlabbatch{1}.spm.tools.ali.unified_segmentation.step1mask = {''}; % specify cost function mask CFM (optional) +matlabbatch{1}.spm.tools.ali.unified_segmentation.step1vox = 2; % Voxel sizes (in mm) +matlabbatch{1}.spm.tools.ali.unified_segmentation.step1fwhm = [8 8 8]; % Smooth: FWHM + +end \ No newline at end of file From 95dd4563324e631ad18ff409f9835aa342082f3c Mon Sep 17 00:00:00 2001 From: Michele MacLean Date: Mon, 15 Mar 2021 16:35:15 -0400 Subject: [PATCH 041/145] rewrite batch structure --- src/batches/setBatchSegmentationDetectLesion.m | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/batches/setBatchSegmentationDetectLesion.m b/src/batches/setBatchSegmentationDetectLesion.m index 5591bf12..7c961d5c 100755 --- a/src/batches/setBatchSegmentationDetectLesion.m +++ b/src/batches/setBatchSegmentationDetectLesion.m @@ -25,13 +25,15 @@ % specify Prior EXTRA class (lesion prior map) lesionPriorMap = fullfile(spmDir, 'toolbox', 'ALI', 'Priors_extraClass', 'wc4prior0.nii'); -matlabbatch{1}.spm.tools.ali.unified_segmentation.step1prior = {lesionPriorMap}; -matlabbatch{1}.spm.tools.ali.unified_segmentation.step1niti = 2; % number of iterations -matlabbatch{1}.spm.tools.ali.unified_segmentation.step1thr_prob = 0.333333333333333; % threshold probability -matlabbatch{1}.spm.tools.ali.unified_segmentation.step1thr_size = 0.8; % threshold size (in cm3) -matlabbatch{1}.spm.tools.ali.unified_segmentation.step1coregister = 1; % coregister in MNI space (yes: 1) -matlabbatch{1}.spm.tools.ali.unified_segmentation.step1mask = {''}; % specify cost function mask CFM (optional) -matlabbatch{1}.spm.tools.ali.unified_segmentation.step1vox = 2; % Voxel sizes (in mm) -matlabbatch{1}.spm.tools.ali.unified_segmentation.step1fwhm = [8 8 8]; % Smooth: FWHM +unified_segmentation.step1prior = {lesionPriorMap}; +unified_segmentation.step1niti = 2; % number of iterations +unified_segmentation.step1thr_prob = 0.333333333333333; % threshold probability +unified_segmentation.step1thr_size = 0.8; % threshold size (in cm3) +unified_segmentation.step1coregister = 1; % coregister in MNI space (yes: 1) +unified_segmentation.step1mask = {''}; % specify cost function mask CFM (optional) +unified_segmentation.step1vox = 2; % Voxel sizes (in mm) +unified_segmentation.step1fwhm = [8 8 8]; % Smooth: FWHM + +matlabbatch{end+1}.spm.tools.ali.unified_segmentation = unified_segmentation; end \ No newline at end of file From 39afc47bea8448f786bdc2bbde2a046d67d947a9 Mon Sep 17 00:00:00 2001 From: Michele MacLean Date: Mon, 15 Mar 2021 17:10:00 -0400 Subject: [PATCH 042/145] change batch names --- .../setBatchLesionAbnormalitiesDetection.m | 14 +++++++ src/batches/setBatchLesionSegmentation.m | 39 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100755 src/batches/setBatchLesionAbnormalitiesDetection.m create mode 100755 src/batches/setBatchLesionSegmentation.m diff --git a/src/batches/setBatchLesionAbnormalitiesDetection.m b/src/batches/setBatchLesionAbnormalitiesDetection.m new file mode 100755 index 00000000..337afe71 --- /dev/null +++ b/src/batches/setBatchLesionAbnormalitiesDetection.m @@ -0,0 +1,14 @@ +%----------------------------------------------------------------------- +% Job saved on 08-Mar-2021 12:47:16 by cfg_util (rev $Rev: 7345 $) +% spm SPM - SPM12 (7771) +% cfg_basicio BasicIO - Unknown +%----------------------------------------------------------------------- +matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3patients = ''; +matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3controls = ''; +matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3Alpha = 0.5; +matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3Lambda = -4; +spmDir = spm('dir'); +lesionMask = fullfile(spmDir, 'toolbox', 'ALI', 'Mask_image', 'mask_controls_vox2mm.nii'); +matlabbatch{1}.spm.tools.ali.outliers_detection.step3mask_thr = 0; +matlabbatch{1}.spm.tools.ali.outliers_detection.step3binary_thr = 0.3; +matlabbatch{1}.spm.tools.ali.outliers_detection.step3binary_size = 0.8; diff --git a/src/batches/setBatchLesionSegmentation.m b/src/batches/setBatchLesionSegmentation.m new file mode 100755 index 00000000..2e519e39 --- /dev/null +++ b/src/batches/setBatchLesionSegmentation.m @@ -0,0 +1,39 @@ +% (C) Copyright 2021 CPP BIDS SPM-pipeline developers + +function matlabbatch = setBatchLesionSegmentation(matlabbatch, BIDS, opt, subID) + % + % Creates a batch to segment the anatomical image for lesion detection + % + % USAGE:: + % + % matlabbatch = setBatchSegmentationDetectLesion(matlabbatch, BIDS, opt, subID) + % + % :param matlabbatch: list of SPM batches + % :type matlabbatch: structure + % + % :returns: - :matlabbatch: (structure) + + printBatchName('Lesion segmentation'); + +% find anatomical file +[anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); +matlabbatch{1}.spm.tools.ali.unified_segmentation.step1data = [anatImage, anatDataDir]; + +% define SPM folder +spmDir = spm('dir'); + +% specify Prior EXTRA class (lesion prior map) +lesionPriorMap = fullfile(spmDir, 'toolbox', 'ALI', 'Priors_extraClass', 'wc4prior0.nii'); + +unified_segmentation.step1prior = {lesionPriorMap}; +unified_segmentation.step1niti = 2; % number of iterations +unified_segmentation.step1thr_prob = 0.333333333333333; % threshold probability +unified_segmentation.step1thr_size = 0.8; % threshold size (in cm3) +unified_segmentation.step1coregister = 1; % coregister in MNI space (yes: 1) +unified_segmentation.step1mask = {''}; % specify cost function mask CFM (optional) +unified_segmentation.step1vox = 2; % Voxel sizes (in mm) +unified_segmentation.step1fwhm = [8 8 8]; % Smooth: FWHM + +matlabbatch{end+1}.spm.tools.ali.unified_segmentation = unified_segmentation; + +end \ No newline at end of file From 129de1fdffd32b7b7dcbc9141a9ab6d4a2c3b04c Mon Sep 17 00:00:00 2001 From: Michele MacLean Date: Mon, 15 Mar 2021 17:14:05 -0400 Subject: [PATCH 043/145] add lesion segmentation workflow --- .../setBatchAbnormalitiesDetectLesion.m | 14 ------ .../setBatchSegmentationDetectLesion.m | 39 ---------------- src/workflows/bidsLesionSegmentation.m | 44 +++++++++++++++++++ 3 files changed, 44 insertions(+), 53 deletions(-) delete mode 100755 src/batches/setBatchAbnormalitiesDetectLesion.m delete mode 100755 src/batches/setBatchSegmentationDetectLesion.m create mode 100755 src/workflows/bidsLesionSegmentation.m diff --git a/src/batches/setBatchAbnormalitiesDetectLesion.m b/src/batches/setBatchAbnormalitiesDetectLesion.m deleted file mode 100755 index 337afe71..00000000 --- a/src/batches/setBatchAbnormalitiesDetectLesion.m +++ /dev/null @@ -1,14 +0,0 @@ -%----------------------------------------------------------------------- -% Job saved on 08-Mar-2021 12:47:16 by cfg_util (rev $Rev: 7345 $) -% spm SPM - SPM12 (7771) -% cfg_basicio BasicIO - Unknown -%----------------------------------------------------------------------- -matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3patients = ''; -matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3controls = ''; -matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3Alpha = 0.5; -matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3Lambda = -4; -spmDir = spm('dir'); -lesionMask = fullfile(spmDir, 'toolbox', 'ALI', 'Mask_image', 'mask_controls_vox2mm.nii'); -matlabbatch{1}.spm.tools.ali.outliers_detection.step3mask_thr = 0; -matlabbatch{1}.spm.tools.ali.outliers_detection.step3binary_thr = 0.3; -matlabbatch{1}.spm.tools.ali.outliers_detection.step3binary_size = 0.8; diff --git a/src/batches/setBatchSegmentationDetectLesion.m b/src/batches/setBatchSegmentationDetectLesion.m deleted file mode 100755 index 7c961d5c..00000000 --- a/src/batches/setBatchSegmentationDetectLesion.m +++ /dev/null @@ -1,39 +0,0 @@ -% (C) Copyright 2021 CPP BIDS SPM-pipeline developers - -function matlabbatch = setBatchSegmentationDetectLesion(matlabbatch, BIDS, opt, subID) - % - % Creates a batch to segment the anatomical image for lesion detection - % - % USAGE:: - % - % matlabbatch = setBatchSegmentationDetectLesion(matlabbatch, BIDS, opt, subID) - % - % :param matlabbatch: list of SPM batches - % :type matlabbatch: structure - % - % :returns: - :matlabbatch: (structure) - - printBatchName('Segmentation for lesion detection'); - -% find anatomical file -[anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); -matlabbatch{1}.spm.tools.ali.unified_segmentation.step1data = [anatImage, anatDataDir]; - -% define SPM folder -spmDir = spm('dir'); - -% specify Prior EXTRA class (lesion prior map) -lesionPriorMap = fullfile(spmDir, 'toolbox', 'ALI', 'Priors_extraClass', 'wc4prior0.nii'); - -unified_segmentation.step1prior = {lesionPriorMap}; -unified_segmentation.step1niti = 2; % number of iterations -unified_segmentation.step1thr_prob = 0.333333333333333; % threshold probability -unified_segmentation.step1thr_size = 0.8; % threshold size (in cm3) -unified_segmentation.step1coregister = 1; % coregister in MNI space (yes: 1) -unified_segmentation.step1mask = {''}; % specify cost function mask CFM (optional) -unified_segmentation.step1vox = 2; % Voxel sizes (in mm) -unified_segmentation.step1fwhm = [8 8 8]; % Smooth: FWHM - -matlabbatch{end+1}.spm.tools.ali.unified_segmentation = unified_segmentation; - -end \ No newline at end of file diff --git a/src/workflows/bidsLesionSegmentation.m b/src/workflows/bidsLesionSegmentation.m new file mode 100755 index 00000000..5f4ff985 --- /dev/null +++ b/src/workflows/bidsLesionSegmentation.m @@ -0,0 +1,44 @@ +% (C) Copyright 2021 CPP BIDS SPM-pipeline developers + +function bidsLesionSegmentation(opt) + % + % Performs segmentation to detect lesions of anatomical image. + % + % USAGE:: + % + % bidsLesionSegmentation(opt) + % + % :param opt: structure or json filename containing the options. See + % ``checkOptions()`` and ``loadAndCheckOptions()``. + % :type opt: structure + % + % Segmentation will be performed using the information provided in the BIDS data set. + % + + + if nargin < 1 + opt = []; + end + + [BIDS, opt, group] = setUpWorkflow(opt, 'lesion segmentation'); + + %% Loop through the groups, subjects, and sessions + for iGroup = 1:length(group) + + groupName = group(iGroup).name; + + parfor iSub = 1:group(iGroup).numSub + + subID = group(iGroup).subNumber{iSub}; + + printProcessingSubject(groupName, iSub, subID); + + matlabbatch = []; + matlabbatch = setBatchLesionSegmentation(matlabbatch, BIDS, opt, subID); + + saveAndRunWorkflow(matlabbatch, 'LesionSegmentation', opt, subID); + + end + end + +end \ No newline at end of file From fa149a9062361cc68bca6a62f493ffc6a42e9a22 Mon Sep 17 00:00:00 2001 From: Michele MacLean Date: Mon, 15 Mar 2021 17:34:26 -0400 Subject: [PATCH 044/145] rm group loop lesion segmentation workflow --- src/workflows/bidsLesionSegmentation.m | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/workflows/bidsLesionSegmentation.m b/src/workflows/bidsLesionSegmentation.m index 5f4ff985..91dde56f 100755 --- a/src/workflows/bidsLesionSegmentation.m +++ b/src/workflows/bidsLesionSegmentation.m @@ -20,25 +20,20 @@ function bidsLesionSegmentation(opt) opt = []; end - [BIDS, opt, group] = setUpWorkflow(opt, 'lesion segmentation'); + [BIDS, opt] = setUpWorkflow(opt, 'lesion segmentation'); - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) + parfor iSub = 1:numel(opt.subjects) - groupName = group(iGroup).name; + subLabel = opt.subjects{iSub}; - parfor iSub = 1:group(iGroup).numSub + printProcessingSubject(iSub, subLabel); - subID = group(iGroup).subNumber{iSub}; + matlabbatch = []; + matlabbatch = setBatchSTC(matlabbatch, BIDS, opt, subLabel); - printProcessingSubject(groupName, iSub, subID); + saveAndRunWorkflow(matlabbatch, 'LesionSegmentation', opt, subLabel); - matlabbatch = []; - matlabbatch = setBatchLesionSegmentation(matlabbatch, BIDS, opt, subID); - - saveAndRunWorkflow(matlabbatch, 'LesionSegmentation', opt, subID); - - end end -end \ No newline at end of file +end + From a1c79246ab76a1f8ea80af832404583677e1ceb3 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 17 Mar 2021 17:41:37 +0100 Subject: [PATCH 045/145] change default folder structure and name for subject level GLM --- src/subject_level/getFFXdir.m | 5 +++-- tests/test_unit_getFFXdir.m | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/subject_level/getFFXdir.m b/src/subject_level/getFFXdir.m index e2d4d4cc..aca0848c 100644 --- a/src/subject_level/getFFXdir.m +++ b/src/subject_level/getFFXdir.m @@ -21,8 +21,9 @@ ffxDir = fullfile(opt.derivativesDir, ... ['sub-', subID], ... 'stats', ... - ['ffx_task-', opt.taskName], ... - ['ffx_space-' opt.space '_FWHM-', num2str(funcFWFM)]); + ['task-', opt.taskName, ... + '_space-' opt.space, ... + '_FWHM-', num2str(funcFWFM)]); if ~exist(ffxDir, 'dir') mkdir(ffxDir); diff --git a/tests/test_unit_getFFXdir.m b/tests/test_unit_getFFXdir.m index 09fecf07..0af5ef1e 100644 --- a/tests/test_unit_getFFXdir.m +++ b/tests/test_unit_getFFXdir.m @@ -18,8 +18,8 @@ function test_getFFXdirBasic() opt = checkOptions(opt); expectedOutput = fullfile(fileparts(mfilename('fullpath')), 'dummyData', 'derivatives', ... - 'cpp_spm', 'sub-01', 'stats', 'ffx_task-funcLocalizer', ... - 'ffx_space-MNI_FWHM-0'); + 'cpp_spm', 'sub-01', 'stats', ... + 'task-funcLocalizer_space-MNI_FWHM-0'); ffxDir = getFFXdir(subLabel, funcFWFM, opt); From 3cf8763ccf6b1c0f644475eaf1a1a4fabf0400ef Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 17 Mar 2021 17:43:20 +0100 Subject: [PATCH 046/145] change default folder structure and name for group level GLM --- src/group_level/getRFXdir.m | 5 +++-- tests/test_unit_getRFXdir.m | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/group_level/getRFXdir.m b/src/group_level/getRFXdir.m index 69e0ff51..1b01e5ff 100644 --- a/src/group_level/getRFXdir.m +++ b/src/group_level/getRFXdir.m @@ -25,8 +25,9 @@ rfxDir = fullfile( ... opt.derivativesDir, ... 'group', ... - ['rfx_task-', opt.taskName], ... - ['rfx_funcFWHM-', num2str(funcFWHM), '_conFWHM-', num2str(conFWHM)]); + ['task-', opt.taskName, ... + '_funcFWHM-', num2str(funcFWHM), ... + '_conFWHM-', num2str(conFWHM)]); if ~exist(rfxDir, 'dir') mkdir(rfxDir); diff --git a/tests/test_unit_getRFXdir.m b/tests/test_unit_getRFXdir.m index cbdaadec..2e746e37 100644 --- a/tests/test_unit_getRFXdir.m +++ b/tests/test_unit_getRFXdir.m @@ -24,8 +24,7 @@ function test_getRFXdirBasic() 'derivatives', ... 'cpp_spm', ... 'group', ... - 'rfx_task-funcLocalizer', ... - 'rfx_funcFWHM-0_conFWHM-0'); + 'task-funcLocalizer_funcFWHM-0_conFWHM-0'); assertEqual(exist(expectedOutput, 'dir'), 7); From 64fdf6af01fdd2c0721a12cb6b3093633e422834 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 17 Mar 2021 18:33:05 +0100 Subject: [PATCH 047/145] update other tests --- tests/createDummyDataSet.sh | 5 +- .../cpp_spm/dataset_description.json | 50 +++++++++++-------- tests/test_setBatchMeanAnatAndMask.m | 6 +-- tests/test_setBatchSmoothConImages.m | 24 +++------ tests/test_setBatchSubjectLevelContrasts.m | 3 +- tests/test_setBatchSubjectLevelResults.m | 3 +- 6 files changed, 42 insertions(+), 49 deletions(-) diff --git a/tests/createDummyDataSet.sh b/tests/createDummyDataSet.sh index 9cea3ea2..fb1edd46 100755 --- a/tests/createDummyDataSet.sh +++ b/tests/createDummyDataSet.sh @@ -95,11 +95,10 @@ do # STATS mkdir $StartDir/sub-$Subject/stats - mkdir $StartDir/sub-$Subject/stats/ffx_task-vismotion/ - ThisDir=$StartDir/sub-$Subject/stats/ffx_task-vismotion/ffx_space-MNI_FWHM-6 + ThisDir=$StartDir/sub-$Subject/stats/task-vismotion_space-MNI_FWHM-6 mkdir $ThisDir - cp $StartDir/sub-01/stats/ffx_task-vismotion/ffx_space-MNI_FWHM-6/SPM.mat $ThisDir + cp $StartDir/sub-01/stats/task-vismotion_space-MNI_FWHM-6/SPM.mat $ThisDir touch $ThisDir/mask.nii diff --git a/tests/dummyData/derivatives/cpp_spm/dataset_description.json b/tests/dummyData/derivatives/cpp_spm/dataset_description.json index c7375505..4af43894 100755 --- a/tests/dummyData/derivatives/cpp_spm/dataset_description.json +++ b/tests/dummyData/derivatives/cpp_spm/dataset_description.json @@ -1,23 +1,29 @@ { - "License": "", - "Authors": [ - "John Doe", - "Charles Darwin", - "Freddy Krueger" - ], - "Acknowledgements": "Thanks to all to tests that failed courageously.", - "HowToAcknowledge": "", - "Funding": [ - "", - "", - "" - ], - "ReferencesAndLinks": [ - "", - "", - "" - ], - "DatasetDOI": "doi:10.18112/openneuro.ds000114.v1.0.1", - "Name": "dummyData", - "BIDSVersion": "1.1.0" -} + "Acknowledgements": "", + "Authors": [""], + "BIDSVersion": "1.4.1", + "DatasetDOI": "", + "DatasetType": "derivative", + "Funding": [""], + "GeneratedBy": [ + { + "Name": "cpp_spm", + "Version": "v0.2.0", + "Container": { + "Type": "", + "Tag": "" + } + } + ], + "HowToAcknowledge": "", + "License": "", + "Name": "cpp_spm outputs", + "ReferencesAndLinks": [""], + "SourceDatasets": [ + { + "DOI": "doi:10.18112/openneuro.ds000114.v1.0.1", + "URL": "", + "Version": "" + } + ] +} \ No newline at end of file diff --git a/tests/test_setBatchMeanAnatAndMask.m b/tests/test_setBatchMeanAnatAndMask.m index 36b8f0c1..092d7398 100644 --- a/tests/test_setBatchMeanAnatAndMask.m +++ b/tests/test_setBatchMeanAnatAndMask.m @@ -40,13 +40,11 @@ function test_setBatchMeanAnatAndMaskBasic() % expectedBatch{2}.spm.util.imcalc.input{1, 1} = fullfile(opt.derivativesDir, 'sub-01', ... 'stats', ... - 'ffx_task-vismotion', ... - 'ffx_space-MNI_FWHM-6', 'mask.nii'); + 'task-vismotion_space-MNI_FWHM-6', 'mask.nii'); expectedBatch{2}.spm.util.imcalc.input{2, 1} = fullfile(opt.derivativesDir, ... 'sub-02', ... 'stats', ... - 'ffx_task-vismotion', ... - 'ffx_space-MNI_FWHM-6', 'mask.nii'); + 'task-vismotion_space-MNI_FWHM-6', 'mask.nii'); expectedBatch{2}.spm.util.imcalc.output = 'meanMask.nii'; expectedBatch{2}.spm.util.imcalc.outdir{1} = pwd; diff --git a/tests/test_setBatchSmoothConImages.m b/tests/test_setBatchSmoothConImages.m index 6579c4ce..ab45af68 100644 --- a/tests/test_setBatchSmoothConImages.m +++ b/tests/test_setBatchSmoothConImages.m @@ -26,20 +26,16 @@ function test_setBatchSmoothConImagesBasic() expectedBatch{1}.spm.spatial.smooth.fwhm = [6 6 6]; expectedBatch{1}.spm.spatial.smooth.prefix = 's6'; expectedBatch{1}.spm.spatial.smooth.data = {fullfile(opt.derivativesDir, 'sub-01', 'stats', ... - 'ffx_task-vismotion', ... - 'ffx_space-MNI_FWHM-6', ... + 'task-vismotion_space-MNI_FWHM-6', ... 'con_0001.nii'); ... fullfile(opt.derivativesDir, 'sub-01', 'stats', ... - 'ffx_task-vismotion', ... - 'ffx_space-MNI_FWHM-6', ... + 'task-vismotion_space-MNI_FWHM-6', ... 'con_0002.nii'); ... fullfile(opt.derivativesDir, 'sub-01', 'stats', ... - 'ffx_task-vismotion', ... - 'ffx_space-MNI_FWHM-6', ... + 'task-vismotion_space-MNI_FWHM-6', ... 'con_0003.nii'); ... fullfile(opt.derivativesDir, 'sub-01', 'stats', ... - 'ffx_task-vismotion', ... - 'ffx_space-MNI_FWHM-6', ... + 'task-vismotion_space-MNI_FWHM-6', ... 'con_0004.nii')}; expectedBatch{1}.spm.spatial.smooth.dtype = 0; expectedBatch{1}.spm.spatial.smooth.im = 0; @@ -47,20 +43,16 @@ function test_setBatchSmoothConImagesBasic() expectedBatch{2}.spm.spatial.smooth.fwhm = [6 6 6]; expectedBatch{2}.spm.spatial.smooth.prefix = 's6'; expectedBatch{2}.spm.spatial.smooth.data = {fullfile(opt.derivativesDir, 'sub-02', 'stats', ... - 'ffx_task-vismotion', ... - 'ffx_space-MNI_FWHM-6', ... + 'task-vismotion_space-MNI_FWHM-6', ... 'con_0001.nii'); ... fullfile(opt.derivativesDir, 'sub-02', 'stats', ... - 'ffx_task-vismotion', ... - 'ffx_space-MNI_FWHM-6', ... + 'task-vismotion_space-MNI_FWHM-6', ... 'con_0002.nii'); ... fullfile(opt.derivativesDir, 'sub-02', 'stats', ... - 'ffx_task-vismotion', ... - 'ffx_space-MNI_FWHM-6', ... + 'task-vismotion_space-MNI_FWHM-6', ... 'con_0003.nii'); ... fullfile(opt.derivativesDir, 'sub-02', 'stats', ... - 'ffx_task-vismotion', ... - 'ffx_space-MNI_FWHM-6', ... + 'task-vismotion_space-MNI_FWHM-6', ... 'con_0004.nii')}; expectedBatch{2}.spm.spatial.smooth.dtype = 0; expectedBatch{2}.spm.spatial.smooth.im = 0; diff --git a/tests/test_setBatchSubjectLevelContrasts.m b/tests/test_setBatchSubjectLevelContrasts.m index 5da6d60b..91e2078e 100644 --- a/tests/test_setBatchSubjectLevelContrasts.m +++ b/tests/test_setBatchSubjectLevelContrasts.m @@ -30,8 +30,7 @@ function test_setBatchSubjectLevelContrastsBasic() expectedBatch{end + 1}.spm.stats.con.spmmat = {fullfile(opt.derivativesDir, ... 'sub-01', ... 'stats', ... - 'ffx_task-vismotion', ... - 'ffx_space-MNI_FWHM-6', ... + 'task-vismotion_space-MNI_FWHM-6', ... 'SPM.mat')}; expectedBatch{end}.spm.stats.con.delete = 1; diff --git a/tests/test_setBatchSubjectLevelResults.m b/tests/test_setBatchSubjectLevelResults.m index d6dce7b0..ce34aae6 100644 --- a/tests/test_setBatchSubjectLevelResults.m +++ b/tests/test_setBatchSubjectLevelResults.m @@ -31,8 +31,7 @@ function test_setBatchSubjectLevelResultsBasic() expectedBatch{end + 1}.spm.stats.results.spmmat = {fullfile(opt.derivativesDir, ... 'sub-01', ... 'stats', ... - 'ffx_task-vismotion', ... - 'ffx_space-MNI_FWHM-6', ... + 'task-vismotion_space-MNI_FWHM-6', ... 'SPM.mat')}; expectedBatch{end}.spm.stats.results.conspec.titlestr = 'VisMot_p-0050_k-0_MC-FWE'; From 0e1e76b717b7e93a323526e8ea01124021cf1df4 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 17 Mar 2021 22:21:41 +0100 Subject: [PATCH 048/145] change subID to subLabel --- src/subject_level/getFFXdir.m | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/subject_level/getFFXdir.m b/src/subject_level/getFFXdir.m index aca0848c..eb10ebb8 100644 --- a/src/subject_level/getFFXdir.m +++ b/src/subject_level/getFFXdir.m @@ -1,15 +1,15 @@ % (C) Copyright 2019 CPP BIDS SPM-pipeline developers -function ffxDir = getFFXdir(subID, funcFWFM, opt) +function ffxDir = getFFXdir(subLabel, funcFWFM, opt) % % Sets the name the FFX directory and creates it if it does not exist % % USAGE:: % - % ffxDir = getFFXdir(subID, funcFWFM, opt) + % ffxDir = getFFXdir(subLabel, funcFWFM, opt) % - % :param subID: - % :type subID: string + % :param subLabel: + % :type subLabel: string % :param funcFWFM: % :type funcFWFM: scalar % :param opt: @@ -19,7 +19,7 @@ % ffxDir = fullfile(opt.derivativesDir, ... - ['sub-', subID], ... + ['sub-', subLabel], ... 'stats', ... ['task-', opt.taskName, ... '_space-' opt.space, ... From a3b1ae2237f46f5bc2d318db50adfe0395488b51 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 17 Mar 2021 22:47:27 +0100 Subject: [PATCH 049/145] create createGlmDirName --- src/utils/createGlmDirName.m | 9 +++++++++ tests/test_unit_createGlmDirName.m | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 src/utils/createGlmDirName.m create mode 100644 tests/test_unit_createGlmDirName.m diff --git a/src/utils/createGlmDirName.m b/src/utils/createGlmDirName.m new file mode 100644 index 00000000..aa2680c3 --- /dev/null +++ b/src/utils/createGlmDirName.m @@ -0,0 +1,9 @@ +% (C) Copyright 2021 CPP BIDS SPM-pipeline developers + +function glmDirName = createGlmDirName(opt, FWHM) + + glmDirName = ['task-', opt.taskName, ... + '_space-' opt.space, ... + '_FWHM-', num2str(FWHM)]; + +end \ No newline at end of file diff --git a/tests/test_unit_createGlmDirName.m b/tests/test_unit_createGlmDirName.m new file mode 100644 index 00000000..e50c5551 --- /dev/null +++ b/tests/test_unit_createGlmDirName.m @@ -0,0 +1,23 @@ +% (C) Copyright 2021 CPP BIDS SPM-pipeline developers + +function test_suite = test_unit_createGlmDirName %#ok<*STOUT> + 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_createGlmDirName() + + FWHM = 6; + opt.taskName = 'funcLocalizer'; + opt.space = 'MNI'; + + glmDirName = createGlmDirName(opt, FWHM); + + expectedOutput = 'task-funcLocalizer_space-MNI_FWHM-6'; + + assertEqual(glmDirName, expectedOutput); + +end \ No newline at end of file From 905ae0b234cc48d972f96cf8201d3d126ba4fc2d Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 17 Mar 2021 22:47:39 +0100 Subject: [PATCH 050/145] move dummy SPM.mat --- .../SPM.mat | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/dummyData/derivatives/cpp_spm/sub-01/stats/{ffx_task-vismotion/ffx_space-MNI_FWHM-6 => task-vismotion_space-MNI_FWHM-6}/SPM.mat (100%) diff --git a/tests/dummyData/derivatives/cpp_spm/sub-01/stats/ffx_task-vismotion/ffx_space-MNI_FWHM-6/SPM.mat b/tests/dummyData/derivatives/cpp_spm/sub-01/stats/task-vismotion_space-MNI_FWHM-6/SPM.mat similarity index 100% rename from tests/dummyData/derivatives/cpp_spm/sub-01/stats/ffx_task-vismotion/ffx_space-MNI_FWHM-6/SPM.mat rename to tests/dummyData/derivatives/cpp_spm/sub-01/stats/task-vismotion_space-MNI_FWHM-6/SPM.mat From 828c787bc605e415f4de08d803dfe71547c25239 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 17 Mar 2021 23:05:44 +0100 Subject: [PATCH 051/145] let subject level glm dir name be described by user --- src/subject_level/getFFXdir.m | 17 ++++--- src/utils/createGlmDirName.m | 12 ++--- tests/dummyData/models/model-nback_smdl.json | 8 +++ .../models/model-vislocalizer_smdl.json | 51 ++++++++++++++----- ...oc_smdl.json => model-vismotion_smdl.json} | 4 +- tests/test_unit_createGlmDirName.m | 6 +-- tests/test_unit_getFFXdir.m | 16 +++--- tests/utils/setOptions.m | 2 + 8 files changed, 78 insertions(+), 38 deletions(-) create mode 100644 tests/dummyData/models/model-nback_smdl.json rename tests/dummyData/models/{model-visMotionLoc_smdl.json => model-vismotion_smdl.json} (89%) diff --git a/src/subject_level/getFFXdir.m b/src/subject_level/getFFXdir.m index eb10ebb8..558c4796 100644 --- a/src/subject_level/getFFXdir.m +++ b/src/subject_level/getFFXdir.m @@ -18,15 +18,20 @@ % :returns: - :ffxDir: (string) % + % get model name + model = spm_jsonread(opt.model.file); + + glmDirName = createGlmDirName(opt, funcFWFM); + + if ~isempty(model.Name) && ~strcmpi(model.Name, opt.taskName) + glmDirName = [glmDirName, '_desc-', strrep(model.Name, ' ', '')]; + end + ffxDir = fullfile(opt.derivativesDir, ... ['sub-', subLabel], ... 'stats', ... - ['task-', opt.taskName, ... - '_space-' opt.space, ... - '_FWHM-', num2str(funcFWFM)]); + glmDirName); - if ~exist(ffxDir, 'dir') - mkdir(ffxDir); - end + spm_mkdir(ffxDir); end diff --git a/src/utils/createGlmDirName.m b/src/utils/createGlmDirName.m index aa2680c3..8406337f 100644 --- a/src/utils/createGlmDirName.m +++ b/src/utils/createGlmDirName.m @@ -1,9 +1,9 @@ % (C) Copyright 2021 CPP BIDS SPM-pipeline developers function glmDirName = createGlmDirName(opt, FWHM) - - glmDirName = ['task-', opt.taskName, ... - '_space-' opt.space, ... - '_FWHM-', num2str(FWHM)]; - -end \ No newline at end of file + + glmDirName = ['task-', opt.taskName, ... + '_space-' opt.space, ... + '_FWHM-', num2str(FWHM)]; + +end diff --git a/tests/dummyData/models/model-nback_smdl.json b/tests/dummyData/models/model-nback_smdl.json new file mode 100644 index 00000000..2691e059 --- /dev/null +++ b/tests/dummyData/models/model-nback_smdl.json @@ -0,0 +1,8 @@ +{ + "Name": "nback MVPA", + "Description": "for folder naming", + "Input": { + "task": "nback" + } +} + \ No newline at end of file diff --git a/tests/dummyData/models/model-vislocalizer_smdl.json b/tests/dummyData/models/model-vislocalizer_smdl.json index 96fc20de..8052224d 100644 --- a/tests/dummyData/models/model-vislocalizer_smdl.json +++ b/tests/dummyData/models/model-vislocalizer_smdl.json @@ -1,45 +1,70 @@ { - "Name": "Motion localizer", - "Description": "contrasts for the motion localizer dataset", + "Name": "vislocalizer", + "Description": "contrasts for the visual localizer", "Input": { - "task": "visMotion" + "task": "vislocalizer" }, "Steps": [ { "Level": "run", "Model": { "X": [ - "trial_type.VisMot", "trial_type.VisStat", "trial_type.missing_condition", - "trans_x", "trans_y", "trans_z", "rot_x", "rot_y", "rot_z" + "trial_type.VisMot", + "trial_type.VisStat", + "trial_type.missing_condition", + "trans_x", + "trans_y", + "trans_z", + "rot_x", + "rot_y", + "rot_z" ] }, - "AutoContrasts": ["trial_type.listening"] + "AutoContrasts": [ + "trial_type.listening" + ] }, { "Level": "subject", - "AutoContrasts": ["trial_type.VisMot", "trial_type.VisStat" ], + "AutoContrasts": [ + "trial_type.VisMot", + "trial_type.VisStat" + ], "Contrasts": [ { "Name": "VisMot_gt_VisStat", "ConditionList": [ - "trial_type.VisMot", "trial_type.VisStat" + "trial_type.VisMot", + "trial_type.VisStat" + ], + "weights": [ + 1, + -1 ], - "weights": [1, -1], "type": "t" }, { "Name": "VisStat_gt_VisMot", "ConditionList": [ - "trial_type.VisMot", "trial_type.VisStat" + "trial_type.VisMot", + "trial_type.VisStat" + ], + "weights": [ + -1, + 1 ], - "weights": [-1, 1], "type": "t" } ] }, { "Level": "dataset", - "AutoContrasts": ["trial_type.VisMot", "trial_type.VisStat", "VisMot_gt_VisStat", "VisStat_gt_VisMot"] + "AutoContrasts": [ + "trial_type.VisMot", + "trial_type.VisStat", + "VisMot_gt_VisStat", + "VisStat_gt_VisMot" + ] } ] -} +} \ No newline at end of file diff --git a/tests/dummyData/models/model-visMotionLoc_smdl.json b/tests/dummyData/models/model-vismotion_smdl.json similarity index 89% rename from tests/dummyData/models/model-visMotionLoc_smdl.json rename to tests/dummyData/models/model-vismotion_smdl.json index aca51a49..c2bb60dc 100644 --- a/tests/dummyData/models/model-visMotionLoc_smdl.json +++ b/tests/dummyData/models/model-vismotion_smdl.json @@ -1,6 +1,6 @@ { - "Name": "Motion localizer", - "Description": "contrasts for the motion localizer dataset", + "Name": "motion", + "Description": "contrasts for the motion dataset", "Input": { "task": "vismotion" }, diff --git a/tests/test_unit_createGlmDirName.m b/tests/test_unit_createGlmDirName.m index e50c5551..c99b3fec 100644 --- a/tests/test_unit_createGlmDirName.m +++ b/tests/test_unit_createGlmDirName.m @@ -9,15 +9,15 @@ end function test_createGlmDirName() - + FWHM = 6; opt.taskName = 'funcLocalizer'; opt.space = 'MNI'; - + glmDirName = createGlmDirName(opt, FWHM); expectedOutput = 'task-funcLocalizer_space-MNI_FWHM-6'; assertEqual(glmDirName, expectedOutput); -end \ No newline at end of file +end diff --git a/tests/test_unit_getFFXdir.m b/tests/test_unit_getFFXdir.m index 0af5ef1e..38df7473 100644 --- a/tests/test_unit_getFFXdir.m +++ b/tests/test_unit_getFFXdir.m @@ -13,13 +13,13 @@ function test_getFFXdirBasic() funcFWFM = 0; subLabel = '01'; - opt = setOptions('funcLocalizer', subLabel); + opt = setOptions('vislocalizer', subLabel); opt = setDerivativesDir(opt); opt = checkOptions(opt); expectedOutput = fullfile(fileparts(mfilename('fullpath')), 'dummyData', 'derivatives', ... 'cpp_spm', 'sub-01', 'stats', ... - 'task-funcLocalizer_space-MNI_FWHM-0'); + 'task-vislocalizer_space-MNI_FWHM-0'); ffxDir = getFFXdir(subLabel, funcFWFM, opt); @@ -27,22 +27,22 @@ function test_getFFXdirBasic() end -function test_getFFXdirMvpa() +function test_getFFXdirUserSpecified() funcFWFM = 6; subLabel = '02'; - opt = setOptions('nBack', subLabel); + opt = setOptions('nback', subLabel); opt.space = 'individual'; opt = setDerivativesDir(opt); opt = checkOptions(opt); - expectedOutput = fullfile(fileparts(mfilename('fullpath')), 'dummyData', 'derivatives', ... - 'cpp_spm', 'sub-02', 'stats', 'ffx_task-nBack', ... - 'ffx_space-individual_FWHM-6'); - ffxDir = getFFXdir(subLabel, funcFWFM, opt); + expectedOutput = fullfile(fileparts(mfilename('fullpath')), 'dummyData', 'derivatives', ... + 'cpp_spm', 'sub-02', 'stats', ... + 'task-nback_space-individual_FWHM-6_desc-nbackMVPA'); + assertEqual(exist(expectedOutput, 'dir'), 7); end diff --git a/tests/utils/setOptions.m b/tests/utils/setOptions.m index b9c9c4d3..4cc15386 100644 --- a/tests/utils/setOptions.m +++ b/tests/utils/setOptions.m @@ -17,6 +17,8 @@ opt.taskName = task; opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), '..', 'dummyData'); + opt.model.file = fullfile(opt.derivativesDir, 'models', ... + ['model-' task '_smdl.json']); end From f22b3f320c80f6153b1bfb3eb3e55fd21503fcf4 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 17 Mar 2021 23:24:00 +0100 Subject: [PATCH 052/145] update RFXdir --- src/group_level/getRFXdir.m | 17 +++++++++++------ src/subject_level/getFFXdir.m | 4 +--- tests/test_unit_getFFXdir.m | 4 ++-- tests/test_unit_getRFXdir.m | 28 ++++++++++++++++++++++++++-- 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/group_level/getRFXdir.m b/src/group_level/getRFXdir.m index 1b01e5ff..7e06c31c 100644 --- a/src/group_level/getRFXdir.m +++ b/src/group_level/getRFXdir.m @@ -22,15 +22,20 @@ % :returns: :rfxDir: (string) Fullpath of the group level directory % + glmDirName = createGlmDirName(opt, funcFWHM); + + glmDirName = [glmDirName, '_conFWHM-', num2str(conFWHM)]; + + model = spm_jsonread(opt.model.file); + if ~isempty(model.Name) && ~strcmpi(model.Name, opt.taskName) + glmDirName = [glmDirName, '_desc-', strrep(model.Name, ' ', '')]; + end + rfxDir = fullfile( ... opt.derivativesDir, ... 'group', ... - ['task-', opt.taskName, ... - '_funcFWHM-', num2str(funcFWHM), ... - '_conFWHM-', num2str(conFWHM)]); + glmDirName); - if ~exist(rfxDir, 'dir') - mkdir(rfxDir); - end + spm_mkdir(rfxDir); end diff --git a/src/subject_level/getFFXdir.m b/src/subject_level/getFFXdir.m index 558c4796..805e5133 100644 --- a/src/subject_level/getFFXdir.m +++ b/src/subject_level/getFFXdir.m @@ -18,11 +18,9 @@ % :returns: - :ffxDir: (string) % - % get model name - model = spm_jsonread(opt.model.file); - glmDirName = createGlmDirName(opt, funcFWFM); + model = spm_jsonread(opt.model.file); if ~isempty(model.Name) && ~strcmpi(model.Name, opt.taskName) glmDirName = [glmDirName, '_desc-', strrep(model.Name, ' ', '')]; end diff --git a/tests/test_unit_getFFXdir.m b/tests/test_unit_getFFXdir.m index 38df7473..20d2cdfd 100644 --- a/tests/test_unit_getFFXdir.m +++ b/tests/test_unit_getFFXdir.m @@ -29,7 +29,7 @@ function test_getFFXdirBasic() function test_getFFXdirUserSpecified() - funcFWFM = 6; + funcFWHM = 6; subLabel = '02'; opt = setOptions('nback', subLabel); @@ -37,7 +37,7 @@ function test_getFFXdirUserSpecified() opt = setDerivativesDir(opt); opt = checkOptions(opt); - ffxDir = getFFXdir(subLabel, funcFWFM, opt); + ffxDir = getFFXdir(subLabel, funcFWHM, opt); expectedOutput = fullfile(fileparts(mfilename('fullpath')), 'dummyData', 'derivatives', ... 'cpp_spm', 'sub-02', 'stats', ... diff --git a/tests/test_unit_getRFXdir.m b/tests/test_unit_getRFXdir.m index 2e746e37..8410f9db 100644 --- a/tests/test_unit_getRFXdir.m +++ b/tests/test_unit_getRFXdir.m @@ -13,7 +13,8 @@ function test_getRFXdirBasic() funcFWHM = 0; conFWHM = 0; - opt = setOptions('funcLocalizer'); + opt = setOptions('vislocalizer'); + opt = checkOptions(opt); opt = setDerivativesDir(opt); rfxDir = getRFXdir(opt, funcFWHM, conFWHM); @@ -24,7 +25,30 @@ function test_getRFXdirBasic() 'derivatives', ... 'cpp_spm', ... 'group', ... - 'task-funcLocalizer_funcFWHM-0_conFWHM-0'); + 'task-vislocalizer_space-MNI_FWHM-0_conFWHM-0'); + + assertEqual(exist(expectedOutput, 'dir'), 7); + +end + +function test_getFFXdirUserSpecified() + + conFWHM = 0; + funcFWHM = 6; + + opt = setOptions('nback'); + opt = checkOptions(opt); + opt = setDerivativesDir(opt); + + rfxDir = getRFXdir(opt, funcFWHM, conFWHM); + + expectedOutput = fullfile( ... + fileparts(mfilename('fullpath')), ... + 'dummyData', ... + 'derivatives', ... + 'cpp_spm', ... + 'group', ... + 'task-nback_space-MNI_FWHM-6_conFWHM-0_desc-nbackMVPA'); assertEqual(exist(expectedOutput, 'dir'), 7); From f64439e8d84f7c4765cabbc816e73f3cfa3970e5 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 17 Mar 2021 23:36:12 +0100 Subject: [PATCH 053/145] update tests --- demos/MoAE/models/model-MoAE_smdl.json | 2 +- .../models/model-vismotion_smdl.json | 32 ++++++++++++++----- tests/test_createAndReturnOnsetFile.m | 7 +--- ...test_unit_getFFXdir.m => test_getFFXdir.m} | 2 +- ...test_unit_getRFXdir.m => test_getRFXdir.m} | 2 +- tests/test_setBatchFactorialDesign.m | 2 -- tests/test_setBatchSmoothConImages.m | 1 - .../test_unit_getGrpLevelContrastToCompute.m | 2 +- tests/test_unit_specifyContrasts.m | 4 --- 9 files changed, 29 insertions(+), 25 deletions(-) rename tests/{test_unit_getFFXdir.m => test_getFFXdir.m} (95%) rename tests/{test_unit_getRFXdir.m => test_getRFXdir.m} (96%) diff --git a/demos/MoAE/models/model-MoAE_smdl.json b/demos/MoAE/models/model-MoAE_smdl.json index 31c83ca4..f9f396db 100644 --- a/demos/MoAE/models/model-MoAE_smdl.json +++ b/demos/MoAE/models/model-MoAE_smdl.json @@ -1,5 +1,5 @@ { - "Name": "Listening", + "Name": "auditory", "Description": "contrasts to compute for the FIL MoAE dataset", "Input": { "task": "auditory" diff --git a/tests/dummyData/models/model-vismotion_smdl.json b/tests/dummyData/models/model-vismotion_smdl.json index c2bb60dc..7c8dd346 100644 --- a/tests/dummyData/models/model-vismotion_smdl.json +++ b/tests/dummyData/models/model-vismotion_smdl.json @@ -1,5 +1,5 @@ { - "Name": "motion", + "Name": "vismotion", "Description": "contrasts for the motion dataset", "Input": { "task": "vismotion" @@ -7,29 +7,45 @@ "Steps": [ { "Level": "subject", - "AutoContrasts": ["trial_type.VisMot", "trial_type.VisStat" ], + "AutoContrasts": [ + "trial_type.VisMot", + "trial_type.VisStat" + ], "Contrasts": [ { "Name": "VisMot_gt_VisStat", "ConditionList": [ - "trial_type.VisMot", "trial_type.VisStat" + "trial_type.VisMot", + "trial_type.VisStat" + ], + "weights": [ + 1, + -1 ], - "weights": [1, -1], "type": "t" }, { "Name": "VisStat_gt_VisMot", "ConditionList": [ - "trial_type.VisMot", "trial_type.VisStat" + "trial_type.VisMot", + "trial_type.VisStat" + ], + "weights": [ + -1, + 1 ], - "weights": [-1, 1], "type": "t" } ] }, { "Level": "dataset", - "AutoContrasts": ["trial_type.VisMot", "trial_type.VisStat", "VisMot_gt_VisStat", "VisStat_gt_VisMot"] + "AutoContrasts": [ + "trial_type.VisMot", + "trial_type.VisStat", + "VisMot_gt_VisStat", + "VisStat_gt_VisMot" + ] } ] -} +} \ No newline at end of file diff --git a/tests/test_createAndReturnOnsetFile.m b/tests/test_createAndReturnOnsetFile.m index 493b6dfe..a31aa574 100644 --- a/tests/test_createAndReturnOnsetFile.m +++ b/tests/test_createAndReturnOnsetFile.m @@ -16,11 +16,6 @@ function test_createAndReturnOnsetFileBasic() iRun = 1; opt = setOptions('vislocalizer', subLabel); - - opt.model.file = fullfile(fileparts(mfilename('fullpath')), ... - 'dummyData', 'models', ... - 'model-vislocalizer_smdl.json'); - opt = checkOptions(opt); [BIDS, opt] = getData(opt); @@ -34,7 +29,7 @@ function test_createAndReturnOnsetFileBasic() expectedFileName = fullfile(fileparts(mfilename('fullpath')), ... 'dummyData', 'derivatives', 'cpp_spm', 'sub-01', 'stats', ... - 'ffx_task-vislocalizer', 'ffx_space-MNI_FWHM-6', ... + 'task-vislocalizer_space-MNI_FWHM-6', ... 'onsets_sub-01_ses-01_task-vislocalizer_events.mat'); assertEqual(exist(onsetFileName, 'file'), 2); diff --git a/tests/test_unit_getFFXdir.m b/tests/test_getFFXdir.m similarity index 95% rename from tests/test_unit_getFFXdir.m rename to tests/test_getFFXdir.m index 20d2cdfd..61440e96 100644 --- a/tests/test_unit_getFFXdir.m +++ b/tests/test_getFFXdir.m @@ -1,6 +1,6 @@ % (C) Copyright 2020 CPP BIDS SPM-pipeline developers -function test_suite = test_unit_getFFXdir %#ok<*STOUT> +function test_suite = test_getFFXdir %#ok<*STOUT> 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/test_unit_getRFXdir.m b/tests/test_getRFXdir.m similarity index 96% rename from tests/test_unit_getRFXdir.m rename to tests/test_getRFXdir.m index 8410f9db..9fa507c3 100644 --- a/tests/test_unit_getRFXdir.m +++ b/tests/test_getRFXdir.m @@ -1,6 +1,6 @@ % (C) Copyright 2020 CPP BIDS SPM-pipeline developers -function test_suite = test_unit_getRFXdir %#ok<*STOUT> +function test_suite = test_getRFXdir %#ok<*STOUT> 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/test_setBatchFactorialDesign.m b/tests/test_setBatchFactorialDesign.m index 5cbb1fef..51186390 100644 --- a/tests/test_setBatchFactorialDesign.m +++ b/tests/test_setBatchFactorialDesign.m @@ -15,8 +15,6 @@ function test_setBatchFactorialDesignBasic() opt = setOptions('vismotion'); opt.subjects = {'01' '02'}; - opt.model.file = fullfile(fileparts(mfilename('fullpath')), ... - 'dummyData', 'models', 'model-visMotionLoc_smdl.json'); opt = checkOptions(opt); diff --git a/tests/test_setBatchSmoothConImages.m b/tests/test_setBatchSmoothConImages.m index ab45af68..1c2dbd29 100644 --- a/tests/test_setBatchSmoothConImages.m +++ b/tests/test_setBatchSmoothConImages.m @@ -15,7 +15,6 @@ function test_setBatchSmoothConImagesBasic() opt = setOptions('vismotion'); opt.subjects = {'01', '02'}; - opt.taskName = 'vismotion'; opt = checkOptions(opt); [~, opt] = getData(opt); diff --git a/tests/test_unit_getGrpLevelContrastToCompute.m b/tests/test_unit_getGrpLevelContrastToCompute.m index 7b1723c5..5947e431 100644 --- a/tests/test_unit_getGrpLevelContrastToCompute.m +++ b/tests/test_unit_getGrpLevelContrastToCompute.m @@ -11,7 +11,7 @@ function test_getGrpLevelContrastToComputeBasic() opt.model.file = fullfile(fileparts(mfilename('fullpath')), ... - 'dummyData', 'models', 'model-visMotionLoc_smdl.json'); + 'dummyData', 'models', 'model-vismotion_smdl.json'); [grpLvlCon, iStep] = getGrpLevelContrastToCompute(opt); diff --git a/tests/test_unit_specifyContrasts.m b/tests/test_unit_specifyContrasts.m index 45fb979c..56cae880 100644 --- a/tests/test_unit_specifyContrasts.m +++ b/tests/test_unit_specifyContrasts.m @@ -18,10 +18,6 @@ function test_specifyContrastsBasic() opt = checkOptions(opt); opt = setDerivativesDir(opt); - opt.model.file = ... - fullfile(fileparts(mfilename('fullpath')), ... - 'dummyData', 'models', 'model-visMotionLoc_smdl.json'); - ffxDir = getFFXdir(subLabel, funcFWFM, opt); contrasts = specifyContrasts(ffxDir, opt.taskName, opt); From 1f443e0f91dac0e993979055d167cd9a5d714d9a Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 17 Mar 2021 23:43:35 +0100 Subject: [PATCH 054/145] lint --- tests/test_setBatchMeanAnatAndMask.m | 56 ++++++++++++---------- tests/test_setBatchSubjectLevelContrasts.m | 6 --- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/tests/test_setBatchMeanAnatAndMask.m b/tests/test_setBatchMeanAnatAndMask.m index 092d7398..6ff60d18 100644 --- a/tests/test_setBatchMeanAnatAndMask.m +++ b/tests/test_setBatchMeanAnatAndMask.m @@ -22,33 +22,39 @@ function test_setBatchMeanAnatAndMaskBasic() matlabbatch = setBatchMeanAnatAndMask(matlabbatch, opt, funcFWHM, pwd); % - expectedBatch{1}.spm.util.imcalc.input{1, 1} = fullfile(opt.derivativesDir, ... - 'sub-01', ... - 'ses-01', ... - 'anat', ... - 'wmsub-01_ses-01_T1w.nii'); - expectedBatch{1}.spm.util.imcalc.input{2, 1} = fullfile(opt.derivativesDir, ... - 'sub-02', ... - 'ses-01', ... - 'anat', ... - 'wmsub-02_ses-01_T1w.nii'); - - expectedBatch{1}.spm.util.imcalc.output = 'meanAnat.nii'; - expectedBatch{1}.spm.util.imcalc.outdir{1} = pwd; - expectedBatch{1}.spm.util.imcalc.expression = '(i1+i2)/2'; + imcalc.input{1, 1} = fullfile(opt.derivativesDir, ... + 'sub-01', ... + 'ses-01', ... + 'anat', ... + 'wmsub-01_ses-01_T1w.nii'); + imcalc.input{2, 1} = fullfile(opt.derivativesDir, ... + 'sub-02', ... + 'ses-01', ... + 'anat', ... + 'wmsub-02_ses-01_T1w.nii'); + + imcalc.output = 'meanAnat.nii'; + imcalc.outdir{1} = pwd; + imcalc.expression = '(i1+i2)/2'; + + expectedBatch{1}.spm.util.imcalc = imcalc; % - expectedBatch{2}.spm.util.imcalc.input{1, 1} = fullfile(opt.derivativesDir, 'sub-01', ... - 'stats', ... - 'task-vismotion_space-MNI_FWHM-6', 'mask.nii'); - expectedBatch{2}.spm.util.imcalc.input{2, 1} = fullfile(opt.derivativesDir, ... - 'sub-02', ... - 'stats', ... - 'task-vismotion_space-MNI_FWHM-6', 'mask.nii'); - - expectedBatch{2}.spm.util.imcalc.output = 'meanMask.nii'; - expectedBatch{2}.spm.util.imcalc.outdir{1} = pwd; - expectedBatch{2}.spm.util.imcalc.expression = '(i1+i2)>0.75*2'; + imcalc.input{1, 1} = fullfile(opt.derivativesDir, 'sub-01', ... + 'stats', ... + 'task-vismotion_space-MNI_FWHM-6', ... + 'mask.nii'); + imcalc.input{2, 1} = fullfile(opt.derivativesDir, ... + 'sub-02', ... + 'stats', ... + 'task-vismotion_space-MNI_FWHM-6', ... + 'mask.nii'); + + imcalc.output = 'meanMask.nii'; + imcalc.outdir{1} = pwd; + imcalc.expression = '(i1+i2)>0.75*2'; + + expectedBatch{2}.spm.util.imcalc = imcalc; assertEqual(matlabbatch, expectedBatch); diff --git a/tests/test_setBatchSubjectLevelContrasts.m b/tests/test_setBatchSubjectLevelContrasts.m index 91e2078e..b7958221 100644 --- a/tests/test_setBatchSubjectLevelContrasts.m +++ b/tests/test_setBatchSubjectLevelContrasts.m @@ -14,12 +14,6 @@ function test_setBatchSubjectLevelContrastsBasic() funcFWHM = 6; opt = setOptions('vismotion', subLabel); - opt.space = 'MNI'; - opt.model.file = ... - fullfile(fileparts(mfilename('fullpath')), ... - 'dummyData', ... - 'models', ... - 'model-visMotionLoc_smdl.json'); opt = setDerivativesDir(opt); opt = checkOptions(opt); From 91f5bd2dd403ceafe539af924f865327f4b94418 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 18 Mar 2021 09:34:22 +0100 Subject: [PATCH 055/145] add function to create valid camelCase strings --- src/utils/converToValidCamelCase.m | 30 ++++++++++++++++++++++++ tests/test_unit_converToValidCamelCase.m | 23 ++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 src/utils/converToValidCamelCase.m create mode 100644 tests/test_unit_converToValidCamelCase.m diff --git a/src/utils/converToValidCamelCase.m b/src/utils/converToValidCamelCase.m new file mode 100644 index 00000000..68eee824 --- /dev/null +++ b/src/utils/converToValidCamelCase.m @@ -0,0 +1,30 @@ +% (C) Copyright 2021 CPP_BIDS developers + +function str = converToValidCamelCase(str) + % + % Removes non alphanumeric characters and uppercase first letter of all + % words but the first + % + % USAGE:: + % + % str = converToValidCamelCase(str) + % + % :param str: + % :type str: string + % + % :returns: + % :str: (string) returns the input with an upper case for first letter + % for all words but the first one (``camelCase``) and + % removes invalid characters (like spaces). + % + % + + % camel case: upper case for first letter for all words but the first one + spaceIdx = regexp(str, '[a-zA-Z0-9]*', 'start'); + str(spaceIdx(2:end)) = upper(str(spaceIdx(2:end))); + + % remove invalid characters + [unvalidCharacters] = regexp(str, '[^a-zA-Z0-9]'); + str(unvalidCharacters) = []; + +end diff --git a/tests/test_unit_converToValidCamelCase.m b/tests/test_unit_converToValidCamelCase.m new file mode 100644 index 00000000..c134f68c --- /dev/null +++ b/tests/test_unit_converToValidCamelCase.m @@ -0,0 +1,23 @@ +% (C) Copyright 2020 CPP_BIDS developers + +function test_suite = test_unit_converToValidCamelCase %#ok<*STOUT> + 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_converToValidCamelCaseBasic() + + str = 'foo bar'; + str = converToValidCamelCase(str); + assertEqual(str, 'fooBar'); + + %% set up + + str = '&|@#-_(!{})01[]%+/=:;.?,\<> visual task'; + str = converToValidCamelCase(str); + assertEqual(str, '01VisualTask'); + +end From 591472a22bb60b03159b19881667c3346a395c19 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 18 Mar 2021 09:37:39 +0100 Subject: [PATCH 056/145] make sure ffx and rfx dir description are vaild alphanumeric camel case --- src/group_level/getRFXdir.m | 2 +- src/subject_level/getFFXdir.m | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/group_level/getRFXdir.m b/src/group_level/getRFXdir.m index 7e06c31c..e26bd1d2 100644 --- a/src/group_level/getRFXdir.m +++ b/src/group_level/getRFXdir.m @@ -28,7 +28,7 @@ model = spm_jsonread(opt.model.file); if ~isempty(model.Name) && ~strcmpi(model.Name, opt.taskName) - glmDirName = [glmDirName, '_desc-', strrep(model.Name, ' ', '')]; + glmDirName = [glmDirName, '_desc-', converToValidCamelCase(model.Name)]; end rfxDir = fullfile( ... diff --git a/src/subject_level/getFFXdir.m b/src/subject_level/getFFXdir.m index 805e5133..bb90b783 100644 --- a/src/subject_level/getFFXdir.m +++ b/src/subject_level/getFFXdir.m @@ -22,7 +22,7 @@ model = spm_jsonread(opt.model.file); if ~isempty(model.Name) && ~strcmpi(model.Name, opt.taskName) - glmDirName = [glmDirName, '_desc-', strrep(model.Name, ' ', '')]; + glmDirName = [glmDirName, '_desc-', converToValidCamelCase(model.Name)]; end ffxDir = fullfile(opt.derivativesDir, ... From 19ac14e7eaf389c71e835519151948ec9e352c3d Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 18 Mar 2021 20:17:51 +0100 Subject: [PATCH 057/145] update credits and use initCppSpm to check dependencies --- .zenodo.json | 9 +++++++-- demos/MoAE/MoAEpilot_run.m | 2 -- demos/openneuro/ds000001_run.m | 2 -- demos/openneuro/ds000114_run.m | 2 -- demos/openneuro/ds001168_run.m | 2 -- initCppSpm.m | 4 ++++ src/utils/checkDependencies.m | 19 +++++++++---------- src/utils/printCredits.m | 3 +++ 8 files changed, 23 insertions(+), 20 deletions(-) diff --git a/.zenodo.json b/.zenodo.json index d6a937ec..80749c74 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -31,11 +31,16 @@ "affiliation": "Université Catholique de Louvain", "name": "Gurtubay, Ane", "orcid": "0000-0003-3824-2219" - }, + }, { "affiliation": "Université Catholique de Louvain", "name": "Falagiarda, Federica", "orcid": "0000-0001-7844-1605" + }, + { + "affiliation": "Université de Montréal", + "name": "MacLean, Michèle", + "orcid": "0000-0002-0174-9326" } ], "keywords": [ @@ -49,4 +54,4 @@ ], "license": "GPL-3", "upload_type": "software" -} +} \ No newline at end of file diff --git a/demos/MoAE/MoAEpilot_run.m b/demos/MoAE/MoAEpilot_run.m index 5ab325ff..cebdf84b 100644 --- a/demos/MoAE/MoAEpilot_run.m +++ b/demos/MoAE/MoAEpilot_run.m @@ -32,8 +32,6 @@ unzip('MoAEpilot.zip', fullfile(WD, 'output')); fprintf(1, ' Done\n\n'); -checkDependencies(); - %% Run batches reportBIDS(opt); bidsCopyRawFolder(opt, 1); diff --git a/demos/openneuro/ds000001_run.m b/demos/openneuro/ds000001_run.m index 75bf98a4..dab263f2 100644 --- a/demos/openneuro/ds000001_run.m +++ b/demos/openneuro/ds000001_run.m @@ -14,8 +14,6 @@ %% Set options opt = ds000001_getOption(); -checkDependencies(); - reportBIDS(opt); bidsCopyRawFolder(opt, 1); diff --git a/demos/openneuro/ds000114_run.m b/demos/openneuro/ds000114_run.m index ad3c9366..086a5205 100644 --- a/demos/openneuro/ds000114_run.m +++ b/demos/openneuro/ds000114_run.m @@ -14,8 +14,6 @@ %% Set options opt = ds000114_getOption(); -checkDependencies(); - %% Run batches reportBIDS(opt); diff --git a/demos/openneuro/ds001168_run.m b/demos/openneuro/ds001168_run.m index 26f3b406..a7194ec2 100644 --- a/demos/openneuro/ds001168_run.m +++ b/demos/openneuro/ds001168_run.m @@ -13,8 +13,6 @@ %% Set options opt = ds001168_getOption(); -checkDependencies(); - %% Run batches reportBIDS(opt); diff --git a/initCppSpm.m b/initCppSpm.m index dbf73369..a9a3f67a 100644 --- a/initCppSpm.m +++ b/initCppSpm.m @@ -14,4 +14,8 @@ function initCppSpm() addpath(fullfile(WD, 'lib', 'bids-matlab')); + checkDependencies(); + + printCredits(); + end diff --git a/src/utils/checkDependencies.m b/src/utils/checkDependencies.m index ac9c46d4..2dd645ce 100644 --- a/src/utils/checkDependencies.m +++ b/src/utils/checkDependencies.m @@ -12,22 +12,16 @@ function checkDependencies() % checkDependencies() % % .. TODO: - % + % - refactor in a several sub functions to cehck each dependency % - need to check other dependencies (bids-matlab, spmup) % - printCredits(); + fprintf('Checking dependencies\n'); SPM_main = 'SPM12'; SPM_sub = '7487'; - nifti_tools_url = ... - ['https://www.mathworks.com/matlabcentral/fileexchange/' ... - '8797-tools-for-nifti-and-analyze-image']; - - fprintf('Checking dependencies\n'); - - % check spm version + %% check spm version try [a, b] = spm('ver'); fprintf(' Using %s %s\n', a, b); @@ -40,9 +34,14 @@ function checkDependencies() catch error('Failed to check the SPM version: Are you sure that SPM is in the matlab path?'); end + spm('defaults', 'fmri'); - % Check the Nifti tools are indeed there. + %% Check the Nifti tools are indeed there. + nifti_tools_url = ... + ['https://www.mathworks.com/matlabcentral/fileexchange/' ... + '8797-tools-for-nifti-and-analyze-image']; + a = which('load_untouch_nii'); if isempty(a) errorStruct.identifier = 'checkDependencies:missingDependency'; diff --git a/src/utils/printCredits.m b/src/utils/printCredits.m index 906fc1c5..0e67fcd2 100644 --- a/src/utils/printCredits.m +++ b/src/utils/printCredits.m @@ -4,12 +4,15 @@ function printCredits() versionNumber = getVersion(); + % TODO: use the .zenodo.json to load contributors contributors = { ... 'Mohamed Rezk', ... 'Remi Gau', ... 'Olivier Collignon', ... 'Ane Gurtubay', ... 'Marco Barilari', ... + 'Michele MacLean', ... + 'Federica Falagiarda ', ... 'Ceren Battal'}; DOI_URL = 'https://doi.org/10.5281/zenodo.3554331.'; From e663b84492620e986d75d2f78f0032ffe5c8f214 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 19 Mar 2021 10:22:54 +0100 Subject: [PATCH 058/145] start creating anat workflow --- src/defaults/checkOptions.m | 2 +- src/getData.m | 8 ++++++-- src/getInfo.m | 11 +++++++---- src/setDerivativesDir.m | 5 ++++- src/utils/saveOptions.m | 14 ++++++++++---- src/workflows/bidsCopyRawFolder.m | 6 +++++- 6 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/defaults/checkOptions.m b/src/defaults/checkOptions.m index 7c9bcb40..c185732c 100644 --- a/src/defaults/checkOptions.m +++ b/src/defaults/checkOptions.m @@ -126,7 +126,7 @@ function checkFields(opt) - if ~isfield(opt, 'taskName') || isempty(opt.taskName) + if isfield(opt, 'taskName') && isempty(opt.taskName) errorStruct.identifier = 'checkOptions:noTask'; errorStruct.message = sprintf( ... diff --git a/src/getData.m b/src/getData.m index c0dca3d6..7277c20f 100644 --- a/src/getData.m +++ b/src/getData.m @@ -39,13 +39,17 @@ type = 'bold'; end - fprintf(1, 'FOR TASK: %s\n', opt.taskName); + if isfield(opt, 'taskName') + fprintf(1, 'FOR TASK: %s\n', opt.taskName); + else + type = 'T1w'; + end % we let SPM figure out what is in this BIDS data set BIDS = bids.layout(derivativesDir); % make sure that the required tasks exist in the data set - if ~ismember(opt.taskName, bids.query(BIDS, 'tasks')) + if isfield(opt, 'taskName') && ~ismember(opt.taskName, bids.query(BIDS, 'tasks')) fprintf('List of tasks present in this dataset:\n'); bids.query(BIDS, 'tasks'); diff --git a/src/getInfo.m b/src/getInfo.m index 92cc6bf4..0ea7af07 100644 --- a/src/getInfo.m +++ b/src/getInfo.m @@ -54,10 +54,13 @@ case 'sessions' - query = struct( ... - 'sub', subLabel, ... - 'task', opt.taskName); - + if isfield(opt, 'taskName') + query = struct( ... + 'sub', subLabel, ... + 'task', opt.taskName); + else + query = struct('sub', subLabel); + end % upate query with pre-specified options % overwrite is set to true in this case because we might want to run % analysis only on certain sessions diff --git a/src/setDerivativesDir.m b/src/setDerivativesDir.m index 04c9c37a..b0cf3183 100644 --- a/src/setDerivativesDir.m +++ b/src/setDerivativesDir.m @@ -77,6 +77,9 @@ opt.derivativesDir = spm_file(opt.derivativesDir, 'cpath'); % Suffix output directory for the saved jobs - opt.jobsDir = fullfile(opt.derivativesDir, 'JOBS', opt.taskName); + opt.jobsDir = fullfile(opt.derivativesDir, 'JOBS'); + if isfield(opt, 'taskName') + opt.jobsDir = fullfile(opt.derivativesDir, 'JOBS', opt.taskName); + end end diff --git a/src/utils/saveOptions.m b/src/utils/saveOptions.m index 84b2da6f..e0ada50c 100644 --- a/src/utils/saveOptions.m +++ b/src/utils/saveOptions.m @@ -14,10 +14,16 @@ function saveOptions(opt) optionDir = fullfile(pwd, 'cfg'); [~, ~, ~] = mkdir(optionDir); - filename = fullfile(optionDir, ['options', ... - '_task-', opt.taskName, ... - '_date-' datestr(now, 'yyyymmddHHMM'), ... - '.json']); + + taskString = ''; + if isfield(opt, 'taskName') + taskString = ['_task-', opt.taskName]; + end + + filename = fullfile(pwd, ['options', ... + taskString, ... + '_date-' datestr(now, 'yyyymmddHHMM'), ... + '.json']); jsonFormat.indent = ' '; spm_jsonwrite(filename, opt, jsonFormat); diff --git a/src/workflows/bidsCopyRawFolder.m b/src/workflows/bidsCopyRawFolder.m index e647e77e..fb75608d 100644 --- a/src/workflows/bidsCopyRawFolder.m +++ b/src/workflows/bidsCopyRawFolder.m @@ -66,7 +66,11 @@ function bidsCopyRawFolder(opt, deleteZippedNii, modalitiesToCopy, unZip) copyTsvJson(rawDir, derivativesDir); %% Loop through the groups, subjects, sessions - [BIDS, opt] = getData(opt, rawDir); + if ismember(modalitiesToCopy, 'func') + [BIDS, opt] = getData(opt, rawDir); + else + [BIDS, opt] = getData(opt, rawDir, 'T1w'); + end for iSub = 1:numel(opt.subjects) From b936859b9553dc76edb2357e57fa12a0466c6ff1 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 19 Mar 2021 11:25:34 +0100 Subject: [PATCH 059/145] improve initCppSpm to run only once per matlab session --- initCppSpm.m | 46 +++++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/initCppSpm.m b/initCppSpm.m index a9a3f67a..32ba2b36 100644 --- a/initCppSpm.m +++ b/initCppSpm.m @@ -1,21 +1,33 @@ % (C) Copyright 2021 CPP BIDS SPM-pipeline developers function initCppSpm() - - % directory with this script becomes the current directory - WD = fileparts(mfilename('fullpath')); - - % we add all the subfunctions that are in the sub directories - addpath(genpath(fullfile(WD, 'src'))); - addpath(genpath(fullfile(WD, 'lib', 'mancoreg'))); - addpath(genpath(fullfile(WD, 'lib', 'NiftiTools'))); - addpath(genpath(fullfile(WD, 'lib', 'spmup'))); - addpath(genpath(fullfile(WD, 'lib', 'utils'))); - - addpath(fullfile(WD, 'lib', 'bids-matlab')); - - checkDependencies(); - - printCredits(); - + + global CPP_SPM_INITIALIZED + + if isempty(CPP_SPM_INITIALIZED) + + % directory with this script becomes the current directory + thisDirectory = fileparts(mfilename('fullpath')); + + % we add all the subfunctions that are in the sub directories + addpath(genpath(fullfile(thisDirectory, 'src'))); + addpath(genpath(fullfile(thisDirectory, 'lib', 'mancoreg'))); + addpath(genpath(fullfile(thisDirectory, 'lib', 'NiftiTools'))); + addpath(genpath(fullfile(thisDirectory, 'lib', 'spmup'))); + addpath(genpath(fullfile(thisDirectory, 'lib', 'utils'))); + + addpath(fullfile(thisDirectory, 'lib', 'bids-matlab')); + + checkDependencies(); + + printCredits(); + + CPP_SPM_INITIALIZED = true(); + + else + fprintf('\n\nCPP_SPM already initialized\n\n'); + + end + + end From 86b989ae59d74eddd0ad90ae3b8ec990d75a6b7c Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 19 Mar 2021 11:25:58 +0100 Subject: [PATCH 060/145] fix saver option to save in cfg folder --- src/utils/saveOptions.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/saveOptions.m b/src/utils/saveOptions.m index e0ada50c..15ca5cfd 100644 --- a/src/utils/saveOptions.m +++ b/src/utils/saveOptions.m @@ -20,7 +20,7 @@ function saveOptions(opt) taskString = ['_task-', opt.taskName]; end - filename = fullfile(pwd, ['options', ... + filename = fullfile(optionDir, ['options', ... taskString, ... '_date-' datestr(now, 'yyyymmddHHMM'), ... '.json']); From 67ca6d01def94594d2e710d31605951c3b0ce686 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 19 Mar 2021 11:26:29 +0100 Subject: [PATCH 061/145] refactor download and convert face rep data function --- ...vert2BIDS.m => dowloadAndConvertFaceRep.m} | 59 ++++++++++--------- 1 file changed, 32 insertions(+), 27 deletions(-) rename demos/spm_face_rep/{face_rep_convert2BIDS.m => dowloadAndConvertFaceRep.m} (78%) diff --git a/demos/spm_face_rep/face_rep_convert2BIDS.m b/demos/spm_face_rep/dowloadAndConvertFaceRep.m similarity index 78% rename from demos/spm_face_rep/face_rep_convert2BIDS.m rename to demos/spm_face_rep/dowloadAndConvertFaceRep.m index fffea5da..8aa3b9f8 100644 --- a/demos/spm_face_rep/face_rep_convert2BIDS.m +++ b/demos/spm_face_rep/dowloadAndConvertFaceRep.m @@ -1,6 +1,6 @@ % (C) Copyright 2020 CPP BIDS SPM-pipeline developers -function face_rep_convert2BIDS() +function dowloadAndConvertFaceRep() % % downloads the fare repetition dataset from SPM and convert it to BIDS % @@ -19,55 +19,60 @@ function face_rep_convert2BIDS() % URL of the data set to download URL = 'http://www.fil.ion.ucl.ac.uk/spm/download/data/face_rep/face_rep.zip'; - % Working directory - WD = fileparts(mfilename('fullpath')); + working_directory = fileparts(mfilename('fullpath')); + + % clean previous runs + try + rmdir(fullfile(working_directory, 'raw'), 's') + rmdir(fullfile(working_directory, 'source'), 's') + catch + end %% Get data fprintf('%-10s:', 'Downloading dataset...'); urlwrite(URL, 'face_rep.zip'); fprintf(1, ' Done\n\n'); - fprintf('%-10s:', 'Unzipping dataset...'); - unzip('face_rep.zip', WD); + unzip('face_rep.zip', working_directory); movefile('face_rep', 'source'); fprintf(1, ' Done\n\n'); %% Create file structure hierarchy - spm_mkdir(WD, 'raw', subject, {'anat', 'func'}); + spm_mkdir(working_directory, 'raw', subject, {'anat', 'func'}); %% Structural MRI - anat_hdr = spm_vol(fullfile(WD, 'source', 'Structural', 'sM03953_0007.img')); + anat_hdr = spm_vol(fullfile(working_directory, 'source', 'Structural', 'sM03953_0007.img')); anat_data = spm_read_vols(anat_hdr); - anat_hdr.fname = fullfile(WD, 'raw', 'sub-01', 'anat', 'sub-01_T1w.nii'); + anat_hdr.fname = fullfile(working_directory, 'raw', 'sub-01', 'anat', 'sub-01_T1w.nii'); spm_write_vol(anat_hdr, anat_data); %% Functional MRI - func_files = spm_select('FPList', fullfile(WD, 'source', 'RawEPI'), '^sM.*\.img$'); + func_files = spm_select('FPList', fullfile(working_directory, 'source', 'RawEPI'), '^sM.*\.img$'); spm_file_merge( ... func_files, ... - fullfile(WD, 'raw', 'sub-01', 'func', ... + fullfile(working_directory, 'raw', 'sub-01', 'func', ... ['sub-01_task-' strrep(task_name, ' ', '') '_bold.nii']), ... 0, ... repetition_time); - delete(fullfile(WD, 'raw', 'sub-01', 'func', ... + delete(fullfile(working_directory, 'raw', 'sub-01', 'func', ... ['sub-01_task-' strrep(task_name, ' ', '') '_bold.mat'])); %% And everything else - create_events_tsv_file(WD, task_name, repetition_time); - create_readme(WD); - create_changelog(WD); - create_datasetdescription(WD, opt); - create_bold_json(WD, task_name, repetition_time, nb_slices, echo_time, opt); + create_events_tsv_file(working_directory, task_name, repetition_time); + create_readme(working_directory); + create_changelog(working_directory); + create_datasetdescription(working_directory, opt); + create_bold_json(working_directory, task_name, repetition_time, nb_slices, echo_time, opt); end -function create_events_tsv_file(WD, task_name, repetition_time) +function create_events_tsv_file(working_directory, task_name, repetition_time) % TODO % add the lag between presentations of each item necessary for the parametric % analysis. - load(fullfile(WD, 'source', 'all_conditions.mat'), ... + load(fullfile(working_directory, 'source', 'all_conditions.mat'), ... 'names', 'onsets', 'durations'); onset_column = []; @@ -94,13 +99,13 @@ function create_events_tsv_file(WD, task_name, repetition_time) 'duration', duration_column, ... 'trial_type', {cellstr(trial_type_column)}); - spm_save(fullfile(WD, 'raw', 'sub-01', 'func', ... + spm_save(fullfile(working_directory, 'raw', 'sub-01', 'func', ... ['sub-01_task-' strrep(task_name, ' ', '') '_events.tsv']), ... tsv_content); end -function create_readme(WD) +function create_readme(working_directory) rdm = { ' ___ ____ __ __' @@ -152,7 +157,7 @@ function create_readme(WD) % TODO % use spm_save to actually write this file? - fid = fopen(fullfile(WD, 'raw', 'README'), 'wt'); + fid = fopen(fullfile(working_directory, 'raw', 'README'), 'wt'); for i = 1:numel(rdm) fprintf(fid, '%s\n', rdm{i}); end @@ -160,12 +165,12 @@ function create_readme(WD) end -function create_changelog(WD) +function create_changelog(working_directory) cg = { ... '1.0.1 2020-11-26', ' - BIDS version.', ... '1.0.0 1999-05-13', ' - Initial release.'}; - fid = fopen(fullfile(WD, 'raw', 'CHANGES'), 'wt'); + fid = fopen(fullfile(working_directory, 'raw', 'CHANGES'), 'wt'); for i = 1:numel(cg) fprintf(fid, '%s\n', cg{i}); @@ -174,7 +179,7 @@ function create_changelog(WD) end -function create_datasetdescription(WD, opt) +function create_datasetdescription(working_directory, opt) descr = struct( ... 'BIDSVersion', '1.4.0', ... @@ -193,13 +198,13 @@ function create_datasetdescription(WD, opt) 'doi:10.1093/cercor/12.2.178'}} ... ); - spm_save(fullfile(WD, 'raw', 'dataset_description.json'), ... + spm_save(fullfile(working_directory, 'raw', 'dataset_description.json'), ... descr, ... opt); end -function create_bold_json(WD, task_name, repetition_time, nb_slices, echo_time, opt) +function create_bold_json(working_directory, task_name, repetition_time, nb_slices, echo_time, opt) acquisition_time = repetition_time - repetition_time / nb_slices; slice_timing = linspace(acquisition_time, 0, nb_slices); @@ -220,7 +225,7 @@ function create_bold_json(WD, task_name, repetition_time, nb_slices, echo_time, 'MagneticFieldStrength', 2); spm_save(fullfile( ... - WD, ... + working_directory, ... 'raw', ... ['task-' strrep(task_name, ' ', '') '_bold.json']), ... task, ... From 95423e327665175e17324f61814a40ac3156e232 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 19 Mar 2021 12:06:49 +0100 Subject: [PATCH 062/145] refactor download and convert face rep --- demos/spm_face_rep/dowloadAndConvertFaceRep.m | 61 ++++++++++--------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/demos/spm_face_rep/dowloadAndConvertFaceRep.m b/demos/spm_face_rep/dowloadAndConvertFaceRep.m index 8aa3b9f8..6188a7f6 100644 --- a/demos/spm_face_rep/dowloadAndConvertFaceRep.m +++ b/demos/spm_face_rep/dowloadAndConvertFaceRep.m @@ -20,59 +20,64 @@ function dowloadAndConvertFaceRep() URL = 'http://www.fil.ion.ucl.ac.uk/spm/download/data/face_rep/face_rep.zip'; working_directory = fileparts(mfilename('fullpath')); + input_dir = fullfile(working_directory, 'inputs', 'source'); + output_dir = fullfile(working_directory, 'outputs', 'raw'); % clean previous runs try - rmdir(fullfile(working_directory, 'raw'), 's') - rmdir(fullfile(working_directory, 'source'), 's') + rmdir(input_dir, 's'); + rmdir(output_dir, 's'); catch end + spm_mkdir(fullfile(working_directory, 'inputs')); + spm_mkdir(output_dir); %% Get data fprintf('%-10s:', 'Downloading dataset...'); urlwrite(URL, 'face_rep.zip'); fprintf(1, ' Done\n\n'); + fprintf('%-10s:', 'Unzipping dataset...'); - unzip('face_rep.zip', working_directory); - movefile('face_rep', 'source'); + unzip('face_rep.zip'); + movefile('face_rep', fullfile(working_directory, 'inputs', 'source')); fprintf(1, ' Done\n\n'); - %% Create file structure hierarchy - spm_mkdir(working_directory, 'raw', subject, {'anat', 'func'}); + %% Create ouput folder structure + spm_mkdir(output_dir, subject, {'anat', 'func'}); %% Structural MRI - anat_hdr = spm_vol(fullfile(working_directory, 'source', 'Structural', 'sM03953_0007.img')); + anat_hdr = spm_vol(fullfile(input_dir, 'Structural', 'sM03953_0007.img')); anat_data = spm_read_vols(anat_hdr); - anat_hdr.fname = fullfile(working_directory, 'raw', 'sub-01', 'anat', 'sub-01_T1w.nii'); + anat_hdr.fname = fullfile(output_dir, 'sub-01', 'anat', 'sub-01_T1w.nii'); spm_write_vol(anat_hdr, anat_data); %% Functional MRI - func_files = spm_select('FPList', fullfile(working_directory, 'source', 'RawEPI'), '^sM.*\.img$'); + func_files = spm_select('FPList', fullfile(input_dir, 'RawEPI'), '^sM.*\.img$'); spm_file_merge( ... func_files, ... - fullfile(working_directory, 'raw', 'sub-01', 'func', ... + fullfile(output_dir, 'sub-01', 'func', ... ['sub-01_task-' strrep(task_name, ' ', '') '_bold.nii']), ... 0, ... repetition_time); - delete(fullfile(working_directory, 'raw', 'sub-01', 'func', ... + delete(fullfile(output_dir, 'sub-01', 'func', ... ['sub-01_task-' strrep(task_name, ' ', '') '_bold.mat'])); %% And everything else - create_events_tsv_file(working_directory, task_name, repetition_time); - create_readme(working_directory); - create_changelog(working_directory); - create_datasetdescription(working_directory, opt); - create_bold_json(working_directory, task_name, repetition_time, nb_slices, echo_time, opt); + create_events_tsv_file(input_dir, output_dir, task_name, repetition_time); + create_readme(output_dir); + create_changelog(output_dir); + create_datasetdescription(output_dir, opt); + create_bold_json(output_dir, task_name, repetition_time, nb_slices, echo_time, opt); end -function create_events_tsv_file(working_directory, task_name, repetition_time) +function create_events_tsv_file(input_dir, output_dir, task_name, repetition_time) % TODO % add the lag between presentations of each item necessary for the parametric % analysis. - load(fullfile(working_directory, 'source', 'all_conditions.mat'), ... + load(fullfile(input_dir, 'all_conditions.mat'), ... 'names', 'onsets', 'durations'); onset_column = []; @@ -99,13 +104,13 @@ function create_events_tsv_file(working_directory, task_name, repetition_time) 'duration', duration_column, ... 'trial_type', {cellstr(trial_type_column)}); - spm_save(fullfile(working_directory, 'raw', 'sub-01', 'func', ... + spm_save(fullfile(output_dir, 'sub-01', 'func', ... ['sub-01_task-' strrep(task_name, ' ', '') '_events.tsv']), ... tsv_content); end -function create_readme(working_directory) +function create_readme(output_dir) rdm = { ' ___ ____ __ __' @@ -157,7 +162,7 @@ function create_readme(working_directory) % TODO % use spm_save to actually write this file? - fid = fopen(fullfile(working_directory, 'raw', 'README'), 'wt'); + fid = fopen(fullfile(output_dir, 'README'), 'wt'); for i = 1:numel(rdm) fprintf(fid, '%s\n', rdm{i}); end @@ -165,12 +170,12 @@ function create_readme(working_directory) end -function create_changelog(working_directory) +function create_changelog(output_dir) cg = { ... '1.0.1 2020-11-26', ' - BIDS version.', ... '1.0.0 1999-05-13', ' - Initial release.'}; - fid = fopen(fullfile(working_directory, 'raw', 'CHANGES'), 'wt'); + fid = fopen(fullfile(output_dir, 'CHANGES'), 'wt'); for i = 1:numel(cg) fprintf(fid, '%s\n', cg{i}); @@ -179,7 +184,7 @@ function create_changelog(working_directory) end -function create_datasetdescription(working_directory, opt) +function create_datasetdescription(output_dir, opt) descr = struct( ... 'BIDSVersion', '1.4.0', ... @@ -198,13 +203,13 @@ function create_datasetdescription(working_directory, opt) 'doi:10.1093/cercor/12.2.178'}} ... ); - spm_save(fullfile(working_directory, 'raw', 'dataset_description.json'), ... + spm_save(fullfile(output_dir, 'dataset_description.json'), ... descr, ... opt); end -function create_bold_json(working_directory, task_name, repetition_time, nb_slices, echo_time, opt) +function create_bold_json(output_dir, task_name, repetition_time, nb_slices, echo_time, opt) acquisition_time = repetition_time - repetition_time / nb_slices; slice_timing = linspace(acquisition_time, 0, nb_slices); @@ -224,9 +229,7 @@ function create_bold_json(working_directory, task_name, repetition_time, nb_slic 'ManufacturersModelName', 'MAGNETOM Vision', ... 'MagneticFieldStrength', 2); - spm_save(fullfile( ... - working_directory, ... - 'raw', ... + spm_save(fullfile(output_dir, ... ['task-' strrep(task_name, ' ', '') '_bold.json']), ... task, ... opt); From f112706cb51d09180640fdca49b423471a95e787 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 19 Mar 2021 16:40:59 +0100 Subject: [PATCH 063/145] remove setting default empty for workflows --- src/workflows/bidsCreateVDM.m | 4 ---- src/workflows/bidsFFX.m | 4 ---- src/workflows/bidsLesionSegmentation.m | 5 ----- src/workflows/bidsRFX.m | 4 ---- src/workflows/bidsRealignReslice.m | 4 ---- src/workflows/bidsRealignUnwarp.m | 4 ---- src/workflows/bidsResliceTpmToFunc.m | 4 ---- src/workflows/bidsResults.m | 4 ---- src/workflows/bidsSTC.m | 4 ---- src/workflows/bidsSegmentSkullStrip.m | 9 +++------ src/workflows/bidsSmoothing.m | 6 ------ src/workflows/bidsSpatialPrepro.m | 4 ---- 12 files changed, 3 insertions(+), 53 deletions(-) diff --git a/src/workflows/bidsCreateVDM.m b/src/workflows/bidsCreateVDM.m index baf622a1..461ed2fe 100644 --- a/src/workflows/bidsCreateVDM.m +++ b/src/workflows/bidsCreateVDM.m @@ -21,10 +21,6 @@ function bidsCreateVDM(opt) % (URL missing) % - if nargin < 1 - opt = []; - end - [BIDS, opt] = setUpWorkflow(opt, 'create voxel displacement map'); parfor iSub = 1:numel(opt.subjects) diff --git a/src/workflows/bidsFFX.m b/src/workflows/bidsFFX.m index e6e75db6..83eb8f0c 100644 --- a/src/workflows/bidsFFX.m +++ b/src/workflows/bidsFFX.m @@ -28,10 +28,6 @@ function bidsFFX(action, opt, funcFWHM) % In this way we can make multiple ffx for different smoothing degrees. % - if nargin < 3 - opt = []; - end - [BIDS, opt] = setUpWorkflow(opt, 'subject level GLM'); if isempty(opt.model.file) diff --git a/src/workflows/bidsLesionSegmentation.m b/src/workflows/bidsLesionSegmentation.m index 91dde56f..c405aef9 100755 --- a/src/workflows/bidsLesionSegmentation.m +++ b/src/workflows/bidsLesionSegmentation.m @@ -15,11 +15,6 @@ function bidsLesionSegmentation(opt) % Segmentation will be performed using the information provided in the BIDS data set. % - - if nargin < 1 - opt = []; - end - [BIDS, opt] = setUpWorkflow(opt, 'lesion segmentation'); parfor iSub = 1:numel(opt.subjects) diff --git a/src/workflows/bidsRFX.m b/src/workflows/bidsRFX.m index f7a66606..f72fde14 100644 --- a/src/workflows/bidsRFX.m +++ b/src/workflows/bidsRFX.m @@ -31,10 +31,6 @@ function bidsRFX(action, opt, funcFWHM, conFWHM) % estimation, Contrast estimation % - if nargin < 2 - opt = []; - end - if nargin < 4 || isempty(funcFWHM) funcFWHM = 0; end diff --git a/src/workflows/bidsRealignReslice.m b/src/workflows/bidsRealignReslice.m index 3ab69d17..d2dd05bf 100644 --- a/src/workflows/bidsRealignReslice.m +++ b/src/workflows/bidsRealignReslice.m @@ -15,10 +15,6 @@ function bidsRealignReslice(opt) % Assumes that ``bidsSTC()`` has already been run. % - if nargin < 1 - opt = []; - end - [BIDS, opt] = setUpWorkflow(opt, 'realign and reslice'); parfor iSub = 1:numel(opt.subjects) diff --git a/src/workflows/bidsRealignUnwarp.m b/src/workflows/bidsRealignUnwarp.m index fc299f3a..5ec1c452 100644 --- a/src/workflows/bidsRealignUnwarp.m +++ b/src/workflows/bidsRealignUnwarp.m @@ -18,10 +18,6 @@ function bidsRealignUnwarp(opt) % maps will be used unless ``opt.useFieldmaps`` is set to ``false``. % - if nargin < 1 - opt = []; - end - [BIDS, opt] = setUpWorkflow(opt, 'realign and unwarp'); parfor iSub = 1:numel(opt.subjects) diff --git a/src/workflows/bidsResliceTpmToFunc.m b/src/workflows/bidsResliceTpmToFunc.m index 561f3398..788c971f 100644 --- a/src/workflows/bidsResliceTpmToFunc.m +++ b/src/workflows/bidsResliceTpmToFunc.m @@ -21,10 +21,6 @@ function bidsResliceTpmToFunc(opt) % as the functional. % - if nargin < 1 - opt = []; - end - [BIDS, opt] = setUpWorkflow(opt, 'reslicing tissue probability maps to functional dimension'); for iSub = 1:numel(opt.subjects) diff --git a/src/workflows/bidsResults.m b/src/workflows/bidsResults.m index 8b129d5e..f90f0ab3 100644 --- a/src/workflows/bidsResults.m +++ b/src/workflows/bidsResults.m @@ -21,10 +21,6 @@ function bidsResults(opt, funcFWHM, conFWHM) % :type conFWHM: scalar % - if nargin < 1 - opt = []; - end - [~, opt] = setUpWorkflow(opt, 'computing GLM results'); matlabbatch = []; diff --git a/src/workflows/bidsSTC.m b/src/workflows/bidsSTC.m index ee83b8e3..58aaf908 100644 --- a/src/workflows/bidsSTC.m +++ b/src/workflows/bidsSTC.m @@ -30,10 +30,6 @@ function bidsSTC(opt) % See the documentation for more information about slice timing correction. % - if nargin < 1 - opt = []; - end - [BIDS, opt] = setUpWorkflow(opt, 'slice timing correction'); parfor iSub = 1:numel(opt.subjects) diff --git a/src/workflows/bidsSegmentSkullStrip.m b/src/workflows/bidsSegmentSkullStrip.m index f29a3a7b..cab0e781 100644 --- a/src/workflows/bidsSegmentSkullStrip.m +++ b/src/workflows/bidsSegmentSkullStrip.m @@ -13,11 +13,8 @@ function bidsSegmentSkullStrip(opt) % :type opt: structure % - if nargin < 1 - opt = []; - end - - [BIDS, opt] = setUpWorkflow(opt, 'segmentation and skulltripping'); + data_suffix = opt.anatReference.type; + [BIDS, opt] = setUpWorkflow(opt, 'segmentation and skulltripping', data_suffix); opt.orderBatches.selectAnat = 1; opt.orderBatches.segment = 2; @@ -34,7 +31,7 @@ function bidsSegmentSkullStrip(opt) % dependency from file selector ('Anatomical') matlabbatch = setBatchSegmentation(matlabbatch, opt); - matlabbatch = setBatchSkullStripping(matlabbatch, BIDS, subLabel, opt); + matlabbatch = setBatchSkullStripping(matlabbatch, BIDS, opt, subLabel); saveAndRunWorkflow(matlabbatch, 'segment_skullstrip', opt, subLabel); diff --git a/src/workflows/bidsSmoothing.m b/src/workflows/bidsSmoothing.m index 199d8d63..05f52964 100644 --- a/src/workflows/bidsSmoothing.m +++ b/src/workflows/bidsSmoothing.m @@ -16,12 +16,6 @@ function bidsSmoothing(funcFWHM, opt) % ``checkOptions()`` and ``loadAndCheckOptions()``. % :type opt: structure - % - - if nargin < 2 - opt = []; - end - [BIDS, opt] = setUpWorkflow(opt, 'smoothing functional data'); parfor iSub = 1:numel(opt.subjects) diff --git a/src/workflows/bidsSpatialPrepro.m b/src/workflows/bidsSpatialPrepro.m index c29ac266..e3a4948d 100644 --- a/src/workflows/bidsSpatialPrepro.m +++ b/src/workflows/bidsSpatialPrepro.m @@ -38,10 +38,6 @@ function bidsSpatialPrepro(opt) % - average T1s across sessions if necessarry % - if nargin < 1 - opt = []; - end - [BIDS, opt] = setUpWorkflow(opt, 'spatial preprocessing'); opt.orderBatches.selectAnat = 1; From ee2d61276545dc09f40a2927328be3fd0acb6622 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 19 Mar 2021 17:06:00 +0100 Subject: [PATCH 064/145] update demos --- demos/MoAE/MoAEpilot_run.m | 2 +- .../options_task-auditory.json | 0 ...ptions_task-auditory_space-individual.json | 0 .../options_task-auditory_unwarp-0.json | 0 ...sk-auditory_unwarp-0_space-individual.json | 0 demos/openneuro/ds000001_run.m | 2 +- demos/openneuro/ds000114_run.m | 2 +- demos/openneuro/ds001168_run.m | 2 +- demos/spm_face_rep/FaceRep_getOption.m | 3 +- demos/spm_face_rep/face_rep_anat.m | 36 +++++++++++++++++++ .../{face_rep_run.m => face_rep_func.m} | 21 +++-------- demos/vismotion/batch.m | 6 +--- 12 files changed, 46 insertions(+), 28 deletions(-) rename demos/MoAE/{cfg => options}/options_task-auditory.json (100%) rename demos/MoAE/{cfg => options}/options_task-auditory_space-individual.json (100%) rename demos/MoAE/{cfg => options}/options_task-auditory_unwarp-0.json (100%) rename demos/MoAE/{cfg => options}/options_task-auditory_unwarp-0_space-individual.json (100%) create mode 100644 demos/spm_face_rep/face_rep_anat.m rename demos/spm_face_rep/{face_rep_run.m => face_rep_func.m} (75%) diff --git a/demos/MoAE/MoAEpilot_run.m b/demos/MoAE/MoAEpilot_run.m index cebdf84b..080cca43 100644 --- a/demos/MoAE/MoAEpilot_run.m +++ b/demos/MoAE/MoAEpilot_run.m @@ -18,7 +18,7 @@ % directory with this script becomes the current directory WD = fileparts(mfilename('fullpath')); -initCppSpm(); +run ../../initCppSpm.m %% Set options opt = MoAEpilot_getOption(); diff --git a/demos/MoAE/cfg/options_task-auditory.json b/demos/MoAE/options/options_task-auditory.json similarity index 100% rename from demos/MoAE/cfg/options_task-auditory.json rename to demos/MoAE/options/options_task-auditory.json diff --git a/demos/MoAE/cfg/options_task-auditory_space-individual.json b/demos/MoAE/options/options_task-auditory_space-individual.json similarity index 100% rename from demos/MoAE/cfg/options_task-auditory_space-individual.json rename to demos/MoAE/options/options_task-auditory_space-individual.json diff --git a/demos/MoAE/cfg/options_task-auditory_unwarp-0.json b/demos/MoAE/options/options_task-auditory_unwarp-0.json similarity index 100% rename from demos/MoAE/cfg/options_task-auditory_unwarp-0.json rename to demos/MoAE/options/options_task-auditory_unwarp-0.json diff --git a/demos/MoAE/cfg/options_task-auditory_unwarp-0_space-individual.json b/demos/MoAE/options/options_task-auditory_unwarp-0_space-individual.json similarity index 100% rename from demos/MoAE/cfg/options_task-auditory_unwarp-0_space-individual.json rename to demos/MoAE/options/options_task-auditory_unwarp-0_space-individual.json diff --git a/demos/openneuro/ds000001_run.m b/demos/openneuro/ds000001_run.m index dab263f2..8c7ceb5d 100644 --- a/demos/openneuro/ds000001_run.m +++ b/demos/openneuro/ds000001_run.m @@ -9,7 +9,7 @@ FWHM = 6; conFWHM = 6; -initCppSpm(); +run ../../initCppSpm.m %% Set options opt = ds000001_getOption(); diff --git a/demos/openneuro/ds000114_run.m b/demos/openneuro/ds000114_run.m index 086a5205..3d76273d 100644 --- a/demos/openneuro/ds000114_run.m +++ b/demos/openneuro/ds000114_run.m @@ -9,7 +9,7 @@ FWHM = 6; conFWHM = 6; -initCppSpm(); +run ../../initCppSpm.m %% Set options opt = ds000114_getOption(); diff --git a/demos/openneuro/ds001168_run.m b/demos/openneuro/ds001168_run.m index a7194ec2..c916e503 100644 --- a/demos/openneuro/ds001168_run.m +++ b/demos/openneuro/ds001168_run.m @@ -8,7 +8,7 @@ % Smoothing to apply FWHM = 6; -initCppSpm(); +run ../../initCppSpm.m %% Set options opt = ds001168_getOption(); diff --git a/demos/spm_face_rep/FaceRep_getOption.m b/demos/spm_face_rep/FaceRep_getOption.m index c20e020a..16886f77 100644 --- a/demos/spm_face_rep/FaceRep_getOption.m +++ b/demos/spm_face_rep/FaceRep_getOption.m @@ -10,8 +10,7 @@ opt.taskName = 'facerepetition'; % The directory where the data are located - opt.dataDir = fullfile(fileparts(mfilename('fullpath')), 'raw'); - + opt.dataDir = fullfile(fileparts(mfilename('fullpath')), 'outputs', 'raw'); opt.model.hrfDerivatives = [1 1]; %% DO NOT TOUCH diff --git a/demos/spm_face_rep/face_rep_anat.m b/demos/spm_face_rep/face_rep_anat.m new file mode 100644 index 00000000..8238fa51 --- /dev/null +++ b/demos/spm_face_rep/face_rep_anat.m @@ -0,0 +1,36 @@ +% (C) Copyright 2019 Remi Gau + +% This script will download the face repetition dataset from the FIL +% and will run the basic preprocessing, FFX and contrasts on it. +% +% This show how an anat only workflow would look like +% + +clear; +clc; + +DownloadData = false; + +run ../../initCppSpm.m + +%% Set options +opt.dataDir = fullfile(fileparts(mfilename('fullpath')), 'outputs', 'raw'); +opt.derivativesDir = fullfile(opt.dataDir, '..', 'derivatives', 'cpp_spm-anat'); +opt = checkOptions(opt); +saveOptions(opt); + +%% Removes previous analysis, gets data and converts it to BIDS +if DownloadData + + dowloadAndConvertFaceRep(); + +end + +%% Run batches +reportBIDS(opt); +bidsCopyRawFolder(opt, 1, 'anat'); + +bidsSegmentSkullStrip(opt); + +% The following do not run on octave for now (because of spmup) +anatomicalQA(opt); diff --git a/demos/spm_face_rep/face_rep_run.m b/demos/spm_face_rep/face_rep_func.m similarity index 75% rename from demos/spm_face_rep/face_rep_run.m rename to demos/spm_face_rep/face_rep_func.m index 23a1885f..40591707 100644 --- a/demos/spm_face_rep/face_rep_run.m +++ b/demos/spm_face_rep/face_rep_func.m @@ -17,35 +17,22 @@ clear; clc; -% Smoothing to apply -FWHM = 8; +FWHM = 6; DownloadData = true; -% URL of the data set to download -% directory with this script becomes the current directory -WD = fileparts(mfilename('fullpath')); - -initCppSpm(); +run ../../initCppSpm.m %% Set options opt = FaceRep_getOption(); %% Removes previous analysis, gets data and converts it to BIDS if DownloadData - try %#ok<*UNRCH> - rmdir('source', 's'); - rmdir('raw', 's'); - catch - end - face_rep_convert2BIDS(); + dowloadAndConvertFaceRep(); end -%% -checkDependencies(); - %% Run batches reportBIDS(opt); bidsCopyRawFolder(opt, 1); @@ -68,4 +55,4 @@ bidsFFX('contrasts', opt, FWHM); % TODO -bidsResults(opt, FWHM); +% bidsResults(opt, FWHM); diff --git a/demos/vismotion/batch.m b/demos/vismotion/batch.m index 8c52b50d..57b45e08 100644 --- a/demos/vismotion/batch.m +++ b/demos/vismotion/batch.m @@ -6,15 +6,11 @@ % directory with this script becomes the current directory WD = fileparts(mfilename('fullpath')); -% we add all the subfunctions that are in the sub directories -addpath(genpath(fullfile(WD, '..', '..', 'src'))); -addpath(genpath(fullfile(WD, '..', '..', 'lib'))); +run ../../initCppSpm.m %% Run batches opt = getOption(); -checkDependencies(); - reportBIDS(opt); bidsCopyRawFolder(opt, 1); From 7586874e4660dbf477964be20536c4d243733e9d Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 19 Mar 2021 17:06:30 +0100 Subject: [PATCH 065/145] update bidsSegmentSkullstrip --- src/workflows/bidsSegmentSkullStrip.m | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/workflows/bidsSegmentSkullStrip.m b/src/workflows/bidsSegmentSkullStrip.m index cab0e781..d990bba3 100644 --- a/src/workflows/bidsSegmentSkullStrip.m +++ b/src/workflows/bidsSegmentSkullStrip.m @@ -13,8 +13,7 @@ function bidsSegmentSkullStrip(opt) % :type opt: structure % - data_suffix = opt.anatReference.type; - [BIDS, opt] = setUpWorkflow(opt, 'segmentation and skulltripping', data_suffix); + [BIDS, opt] = setUpWorkflow(opt, 'segmentation and skulltripping'); opt.orderBatches.selectAnat = 1; opt.orderBatches.segment = 2; From 58253b3ed96651c5b018f4ff5d1ae49e194cad99 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 19 Mar 2021 17:32:36 +0100 Subject: [PATCH 066/145] update demo folders --- demos/MoAE/MoAEpilot_getOption.m | 3 +- demos/MoAE/MoAEpilot_run.m | 54 +++++++++++++------ ...aceRep.m => dowload_convert_face_rep_ds.m} | 2 +- demos/spm_face_rep/face_rep_anat.m | 6 +-- demos/spm_face_rep/face_rep_func.m | 6 +-- tests/utils/setOptions.m | 12 ++--- 6 files changed, 53 insertions(+), 30 deletions(-) rename demos/spm_face_rep/{dowloadAndConvertFaceRep.m => dowload_convert_face_rep_ds.m} (99%) diff --git a/demos/MoAE/MoAEpilot_getOption.m b/demos/MoAE/MoAEpilot_getOption.m index fc448180..510b569e 100644 --- a/demos/MoAE/MoAEpilot_getOption.m +++ b/demos/MoAE/MoAEpilot_getOption.m @@ -10,8 +10,7 @@ opt.taskName = 'auditory'; % The directory where the data are located - opt.dataDir = fullfile(fileparts(mfilename('fullpath')), 'output', 'MoAEpilot'); - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath'))); + opt.dataDir = fullfile(fileparts(mfilename('fullpath')), 'inputs', 'raw'); % Uncomment the lines below to run preprocessing % - don't use realign and unwarp diff --git a/demos/MoAE/MoAEpilot_run.m b/demos/MoAE/MoAEpilot_run.m index 080cca43..12fe051d 100644 --- a/demos/MoAE/MoAEpilot_run.m +++ b/demos/MoAE/MoAEpilot_run.m @@ -12,33 +12,25 @@ % Smoothing to apply FWHM = 6; -% URL of the data set to download -URL = 'http://www.fil.ion.ucl.ac.uk/spm/download/data/MoAEpilot/MoAEpilot.bids.zip'; - -% directory with this script becomes the current directory -WD = fileparts(mfilename('fullpath')); +downloadData = true; run ../../initCppSpm.m %% Set options opt = MoAEpilot_getOption(); -%% Get data -fprintf('%-10s:', 'Downloading dataset...'); -urlwrite(URL, 'MoAEpilot.zip'); -fprintf(1, ' Done\n\n'); - -fprintf('%-10s:', 'Unzipping dataset...'); -unzip('MoAEpilot.zip', fullfile(WD, 'output')); -fprintf(1, ' Done\n\n'); +dowload_MoAE_ds(downloadData); %% Run batches reportBIDS(opt); bidsCopyRawFolder(opt, 1); % In case you just want to run segmentation and skull stripping -% Skull stripping is also included in 'bidsSpatialPrepro' -% bidsSegmentSkullStrip(opt); +% +% bidsSegmentSkullStrip(opt); +% +% NOTE: skull stripping is also included in 'bidsSpatialPrepro' + bidsSTC(opt); @@ -55,3 +47,35 @@ % bidsFFX('specifyAndEstimate', opt, FWHM); % bidsFFX('contrasts', opt, FWHM); % bidsResults(opt, FWHM); + +%% +function dowload_MoAE_ds(downloadData) + + + if downloadData + + % URL of the data set to download + URL = 'http://www.fil.ion.ucl.ac.uk/spm/download/data/MoAEpilot/MoAEpilot.bids.zip'; + + working_directory = fileparts(mfilename('fullpath')); + + % clean previous runs + if exist(fullfile(working_directory, 'inputs'), 'dir') + rmdir(fullfile(working_directory, 'inputs'), 's'); + end + + spm_mkdir(fullfile(working_directory, 'inputs')); + + %% Get data + fprintf('%-10s:', 'Downloading dataset...'); + urlwrite(URL, 'MoAEpilot.zip'); + fprintf(1, ' Done\n\n'); + + fprintf('%-10s:', 'Unzipping dataset...'); + unzip('MoAEpilot.zip'); + movefile('MoAEpilot', fullfile(working_directory, 'inputs', 'raw')) + fprintf(1, ' Done\n\n'); + + end + +end \ No newline at end of file diff --git a/demos/spm_face_rep/dowloadAndConvertFaceRep.m b/demos/spm_face_rep/dowload_convert_face_rep_ds.m similarity index 99% rename from demos/spm_face_rep/dowloadAndConvertFaceRep.m rename to demos/spm_face_rep/dowload_convert_face_rep_ds.m index 6188a7f6..641237f8 100644 --- a/demos/spm_face_rep/dowloadAndConvertFaceRep.m +++ b/demos/spm_face_rep/dowload_convert_face_rep_ds.m @@ -1,6 +1,6 @@ % (C) Copyright 2020 CPP BIDS SPM-pipeline developers -function dowloadAndConvertFaceRep() +function dowload_convert_face_rep_ds() % % downloads the fare repetition dataset from SPM and convert it to BIDS % diff --git a/demos/spm_face_rep/face_rep_anat.m b/demos/spm_face_rep/face_rep_anat.m index 8238fa51..0d4db0d5 100644 --- a/demos/spm_face_rep/face_rep_anat.m +++ b/demos/spm_face_rep/face_rep_anat.m @@ -9,7 +9,7 @@ clear; clc; -DownloadData = false; +downloadData = false; run ../../initCppSpm.m @@ -20,9 +20,9 @@ saveOptions(opt); %% Removes previous analysis, gets data and converts it to BIDS -if DownloadData +if downloadData - dowloadAndConvertFaceRep(); + dowload_convert_face_rep_ds(); end diff --git a/demos/spm_face_rep/face_rep_func.m b/demos/spm_face_rep/face_rep_func.m index 40591707..71652b7a 100644 --- a/demos/spm_face_rep/face_rep_func.m +++ b/demos/spm_face_rep/face_rep_func.m @@ -19,7 +19,7 @@ FWHM = 6; -DownloadData = true; +downloadData = true; run ../../initCppSpm.m @@ -27,9 +27,9 @@ opt = FaceRep_getOption(); %% Removes previous analysis, gets data and converts it to BIDS -if DownloadData +if downloadData - dowloadAndConvertFaceRep(); + dowload_convert_face_rep_ds(); end diff --git a/tests/utils/setOptions.m b/tests/utils/setOptions.m index 4cc15386..2204c340 100644 --- a/tests/utils/setOptions.m +++ b/tests/utils/setOptions.m @@ -2,13 +2,13 @@ function opt = setOptions(task, subLabel) + thisDir = fileparts(mfilename('fullpath')); + if strcmp(task, 'MoAE') - opt.dataDir = fullfile( ... - fileparts(mfilename('fullpath')), ... - '..', '..', 'demos', 'MoAE', 'output', 'MoAEpilot'); - opt.model.file = fullfile( ... - fileparts(mfilename('fullpath')), ... + opt.dataDir = fullfile(thisDir, ... + '..', '..', 'demos', 'MoAE', 'inputs', 'raw'); + opt.model.file = fullfile((thisDir, ... '..', '..', 'demos', 'MoAE', 'models', 'model-MoAE_smdl.json'); opt.taskName = 'auditory'; @@ -16,7 +16,7 @@ else opt.taskName = task; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), '..', 'dummyData'); + opt.derivativesDir = fullfile((thisDir, '..', 'dummyData'); opt.model.file = fullfile(opt.derivativesDir, 'models', ... ['model-' task '_smdl.json']); From e6ff203b39e151602d10dfc8857e3b4987934906 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 19 Mar 2021 17:34:49 +0100 Subject: [PATCH 067/145] update CI folders --- .github/workflows/run_system_tests.yml | 9 +++++---- .gitignore | 7 +++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/run_system_tests.yml b/.github/workflows/run_system_tests.yml index 9ae64cf0..8f0f845e 100644 --- a/.github/workflows/run_system_tests.yml +++ b/.github/workflows/run_system_tests.yml @@ -62,10 +62,11 @@ jobs: - name: Prepare data run: | - output_folder='demos/MoAE/output/' - mkdir $output_folder - curl http://www.fil.ion.ucl.ac.uk/spm/download/data/MoAEpilot/MoAEpilot.bids.zip --output $output_folder'MoAEpilot.zip' - unzip $output_folder'MoAEpilot.zip' -d $output_folder + inputs_folder='demos/MoAE/inputs/' + mkdir $inputs_folder + curl http://www.fil.ion.ucl.ac.uk/spm/download/data/MoAEpilot/MoAEpilot.bids.zip --output $inputs_folder'MoAEpilot.zip' + unzip $inputs_folder'MoAEpilot.zip' -d $inputs_folder + mv $inputs_folder/MoAEpilot $inputs_folder/raw - name: Run system tests run: | diff --git a/.gitignore b/.gitignore index 0dd9ae12..63575b8a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,10 +14,9 @@ onsets*_events.mat # files in the demo folder related to running the demo analysis demos/*/*.zip -demos/*/derivatives/* -demos/MoAE/output/* -demos/spm*/raw -demos/spm*/source +demos/*/outputs/ +demos/*/inputs/ +demos/*/cfg/*.json # test folder and dummy data tests/cfg/*.json From fc27b7c3bbe40472028261119a205e1cff98cb3f Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 19 Mar 2021 18:24:15 +0100 Subject: [PATCH 068/145] move dummy SPM.mat --- tests/createDummyDataSet.sh | 2 +- .../task-vismotion_space-MNI_FWHM-6 => }/SPM.mat | Bin 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/dummyData/{derivatives/cpp_spm/sub-01/stats/task-vismotion_space-MNI_FWHM-6 => }/SPM.mat (100%) diff --git a/tests/createDummyDataSet.sh b/tests/createDummyDataSet.sh index fb1edd46..b841a19e 100755 --- a/tests/createDummyDataSet.sh +++ b/tests/createDummyDataSet.sh @@ -98,7 +98,7 @@ do ThisDir=$StartDir/sub-$Subject/stats/task-vismotion_space-MNI_FWHM-6 mkdir $ThisDir - cp $StartDir/sub-01/stats/task-vismotion_space-MNI_FWHM-6/SPM.mat $ThisDir + cp dummyData/SPM.mat $ThisDir touch $ThisDir/mask.nii diff --git a/tests/dummyData/derivatives/cpp_spm/sub-01/stats/task-vismotion_space-MNI_FWHM-6/SPM.mat b/tests/dummyData/SPM.mat similarity index 100% rename from tests/dummyData/derivatives/cpp_spm/sub-01/stats/task-vismotion_space-MNI_FWHM-6/SPM.mat rename to tests/dummyData/SPM.mat From ffd9a1f2b6cf985685c605f87d65932b9b49818a Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 19 Mar 2021 19:15:05 +0100 Subject: [PATCH 069/145] fix tests and lint --- demos/MoAE/MoAEpilot_run.m | 60 +++++++++---------- demos/openneuro/ds000001_run.m | 2 +- demos/openneuro/ds000114_run.m | 2 +- demos/openneuro/ds001168_run.m | 2 +- .../dowload_convert_face_rep_ds.m | 8 +-- demos/spm_face_rep/face_rep_anat.m | 2 +- demos/spm_face_rep/face_rep_func.m | 2 +- demos/vismotion/batch.m | 2 +- initCppSpm.m | 57 +++++++++--------- src/utils/converToValidCamelCase.m | 48 +++++++-------- src/utils/saveOptions.m | 6 +- src/workflows/bidsLesionSegmentation.m | 3 +- .../sub-01/ses-01/anat/sub-01_ses-01_T1w.json | 9 --- tests/test_unit_converToValidCamelCase.m | 28 ++++----- tests/test_unit_getFuncVoxelDims.m | 2 +- tests/utils/setOptions.m | 4 +- 16 files changed, 112 insertions(+), 125 deletions(-) delete mode 100755 tests/dummyData/derivatives/cpp_spm/sub-01/ses-01/anat/sub-01_ses-01_T1w.json diff --git a/demos/MoAE/MoAEpilot_run.m b/demos/MoAE/MoAEpilot_run.m index 12fe051d..2ffa2007 100644 --- a/demos/MoAE/MoAEpilot_run.m +++ b/demos/MoAE/MoAEpilot_run.m @@ -14,7 +14,7 @@ downloadData = true; -run ../../initCppSpm.m +run ../../initCppSpm.m; %% Set options opt = MoAEpilot_getOption(); @@ -27,11 +27,10 @@ % In case you just want to run segmentation and skull stripping % -% bidsSegmentSkullStrip(opt); +% bidsSegmentSkullStrip(opt); % % NOTE: skull stripping is also included in 'bidsSpatialPrepro' - bidsSTC(opt); bidsSpatialPrepro(opt); @@ -50,32 +49,31 @@ %% function dowload_MoAE_ds(downloadData) - - - if downloadData - - % URL of the data set to download - URL = 'http://www.fil.ion.ucl.ac.uk/spm/download/data/MoAEpilot/MoAEpilot.bids.zip'; - - working_directory = fileparts(mfilename('fullpath')); - - % clean previous runs - if exist(fullfile(working_directory, 'inputs'), 'dir') - rmdir(fullfile(working_directory, 'inputs'), 's'); - end - - spm_mkdir(fullfile(working_directory, 'inputs')); - - %% Get data - fprintf('%-10s:', 'Downloading dataset...'); - urlwrite(URL, 'MoAEpilot.zip'); - fprintf(1, ' Done\n\n'); - - fprintf('%-10s:', 'Unzipping dataset...'); - unzip('MoAEpilot.zip'); - movefile('MoAEpilot', fullfile(working_directory, 'inputs', 'raw')) - fprintf(1, ' Done\n\n'); - + + if downloadData + + % URL of the data set to download + URL = 'http://www.fil.ion.ucl.ac.uk/spm/download/data/MoAEpilot/MoAEpilot.bids.zip'; + + working_directory = fileparts(mfilename('fullpath')); + + % clean previous runs + if exist(fullfile(working_directory, 'inputs'), 'dir') + rmdir(fullfile(working_directory, 'inputs'), 's'); end - -end \ No newline at end of file + + spm_mkdir(fullfile(working_directory, 'inputs')); + + %% Get data + fprintf('%-10s:', 'Downloading dataset...'); + urlwrite(URL, 'MoAEpilot.zip'); + fprintf(1, ' Done\n\n'); + + fprintf('%-10s:', 'Unzipping dataset...'); + unzip('MoAEpilot.zip'); + movefile('MoAEpilot', fullfile(working_directory, 'inputs', 'raw')); + fprintf(1, ' Done\n\n'); + + end + +end diff --git a/demos/openneuro/ds000001_run.m b/demos/openneuro/ds000001_run.m index 8c7ceb5d..8e1ce6fc 100644 --- a/demos/openneuro/ds000001_run.m +++ b/demos/openneuro/ds000001_run.m @@ -9,7 +9,7 @@ FWHM = 6; conFWHM = 6; -run ../../initCppSpm.m +run ../../initCppSpm.m; %% Set options opt = ds000001_getOption(); diff --git a/demos/openneuro/ds000114_run.m b/demos/openneuro/ds000114_run.m index 3d76273d..56f61cfd 100644 --- a/demos/openneuro/ds000114_run.m +++ b/demos/openneuro/ds000114_run.m @@ -9,7 +9,7 @@ FWHM = 6; conFWHM = 6; -run ../../initCppSpm.m +run ../../initCppSpm.m; %% Set options opt = ds000114_getOption(); diff --git a/demos/openneuro/ds001168_run.m b/demos/openneuro/ds001168_run.m index c916e503..701c88c1 100644 --- a/demos/openneuro/ds001168_run.m +++ b/demos/openneuro/ds001168_run.m @@ -8,7 +8,7 @@ % Smoothing to apply FWHM = 6; -run ../../initCppSpm.m +run ../../initCppSpm.m; %% Set options opt = ds001168_getOption(); diff --git a/demos/spm_face_rep/dowload_convert_face_rep_ds.m b/demos/spm_face_rep/dowload_convert_face_rep_ds.m index 641237f8..a57bc975 100644 --- a/demos/spm_face_rep/dowload_convert_face_rep_ds.m +++ b/demos/spm_face_rep/dowload_convert_face_rep_ds.m @@ -22,11 +22,11 @@ function dowload_convert_face_rep_ds() working_directory = fileparts(mfilename('fullpath')); input_dir = fullfile(working_directory, 'inputs', 'source'); output_dir = fullfile(working_directory, 'outputs', 'raw'); - + % clean previous runs try - rmdir(input_dir, 's'); - rmdir(output_dir, 's'); + rmdir(input_dir, 's'); + rmdir(output_dir, 's'); catch end spm_mkdir(fullfile(working_directory, 'inputs')); @@ -36,7 +36,7 @@ function dowload_convert_face_rep_ds() fprintf('%-10s:', 'Downloading dataset...'); urlwrite(URL, 'face_rep.zip'); fprintf(1, ' Done\n\n'); - + fprintf('%-10s:', 'Unzipping dataset...'); unzip('face_rep.zip'); movefile('face_rep', fullfile(working_directory, 'inputs', 'source')); diff --git a/demos/spm_face_rep/face_rep_anat.m b/demos/spm_face_rep/face_rep_anat.m index 0d4db0d5..2d93eb73 100644 --- a/demos/spm_face_rep/face_rep_anat.m +++ b/demos/spm_face_rep/face_rep_anat.m @@ -11,7 +11,7 @@ downloadData = false; -run ../../initCppSpm.m +run ../../initCppSpm.m; %% Set options opt.dataDir = fullfile(fileparts(mfilename('fullpath')), 'outputs', 'raw'); diff --git a/demos/spm_face_rep/face_rep_func.m b/demos/spm_face_rep/face_rep_func.m index 71652b7a..4d37afcc 100644 --- a/demos/spm_face_rep/face_rep_func.m +++ b/demos/spm_face_rep/face_rep_func.m @@ -21,7 +21,7 @@ downloadData = true; -run ../../initCppSpm.m +run ../../initCppSpm.m; %% Set options opt = FaceRep_getOption(); diff --git a/demos/vismotion/batch.m b/demos/vismotion/batch.m index 57b45e08..ce2ef814 100644 --- a/demos/vismotion/batch.m +++ b/demos/vismotion/batch.m @@ -6,7 +6,7 @@ % directory with this script becomes the current directory WD = fileparts(mfilename('fullpath')); -run ../../initCppSpm.m +run ../../initCppSpm.m; %% Run batches opt = getOption(); diff --git a/initCppSpm.m b/initCppSpm.m index 32ba2b36..c1d02232 100644 --- a/initCppSpm.m +++ b/initCppSpm.m @@ -1,33 +1,32 @@ % (C) Copyright 2021 CPP BIDS SPM-pipeline developers function initCppSpm() - - global CPP_SPM_INITIALIZED - - if isempty(CPP_SPM_INITIALIZED) - - % directory with this script becomes the current directory - thisDirectory = fileparts(mfilename('fullpath')); - - % we add all the subfunctions that are in the sub directories - addpath(genpath(fullfile(thisDirectory, 'src'))); - addpath(genpath(fullfile(thisDirectory, 'lib', 'mancoreg'))); - addpath(genpath(fullfile(thisDirectory, 'lib', 'NiftiTools'))); - addpath(genpath(fullfile(thisDirectory, 'lib', 'spmup'))); - addpath(genpath(fullfile(thisDirectory, 'lib', 'utils'))); - - addpath(fullfile(thisDirectory, 'lib', 'bids-matlab')); - - checkDependencies(); - - printCredits(); - - CPP_SPM_INITIALIZED = true(); - - else - fprintf('\n\nCPP_SPM already initialized\n\n'); - - end - - + + global CPP_SPM_INITIALIZED + + if isempty(CPP_SPM_INITIALIZED) + + % directory with this script becomes the current directory + thisDirectory = fileparts(mfilename('fullpath')); + + % we add all the subfunctions that are in the sub directories + addpath(genpath(fullfile(thisDirectory, 'src'))); + addpath(genpath(fullfile(thisDirectory, 'lib', 'mancoreg'))); + addpath(genpath(fullfile(thisDirectory, 'lib', 'NiftiTools'))); + addpath(genpath(fullfile(thisDirectory, 'lib', 'spmup'))); + addpath(genpath(fullfile(thisDirectory, 'lib', 'utils'))); + + addpath(fullfile(thisDirectory, 'lib', 'bids-matlab')); + + checkDependencies(); + + printCredits(); + + CPP_SPM_INITIALIZED = true(); + + else + fprintf('\n\nCPP_SPM already initialized\n\n'); + + end + end diff --git a/src/utils/converToValidCamelCase.m b/src/utils/converToValidCamelCase.m index 68eee824..99642b99 100644 --- a/src/utils/converToValidCamelCase.m +++ b/src/utils/converToValidCamelCase.m @@ -1,30 +1,30 @@ -% (C) Copyright 2021 CPP_BIDS developers +% (C) Copyright 2021 CPP BIDS SPM-pipeline developers function str = converToValidCamelCase(str) - % - % Removes non alphanumeric characters and uppercase first letter of all - % words but the first - % - % USAGE:: - % - % str = converToValidCamelCase(str) - % - % :param str: - % :type str: string - % - % :returns: - % :str: (string) returns the input with an upper case for first letter - % for all words but the first one (``camelCase``) and - % removes invalid characters (like spaces). - % - % + % + % Removes non alphanumeric characters and uppercase first letter of all + % words but the first + % + % USAGE:: + % + % str = converToValidCamelCase(str) + % + % :param str: + % :type str: string + % + % :returns: + % :str: (string) returns the input with an upper case for first letter + % for all words but the first one (``camelCase``) and + % removes invalid characters (like spaces). + % + % - % camel case: upper case for first letter for all words but the first one - spaceIdx = regexp(str, '[a-zA-Z0-9]*', 'start'); - str(spaceIdx(2:end)) = upper(str(spaceIdx(2:end))); + % camel case: upper case for first letter for all words but the first one + spaceIdx = regexp(str, '[a-zA-Z0-9]*', 'start'); + str(spaceIdx(2:end)) = upper(str(spaceIdx(2:end))); - % remove invalid characters - [unvalidCharacters] = regexp(str, '[^a-zA-Z0-9]'); - str(unvalidCharacters) = []; + % remove invalid characters + [unvalidCharacters] = regexp(str, '[^a-zA-Z0-9]'); + str(unvalidCharacters) = []; end diff --git a/src/utils/saveOptions.m b/src/utils/saveOptions.m index 15ca5cfd..0334f275 100644 --- a/src/utils/saveOptions.m +++ b/src/utils/saveOptions.m @@ -21,9 +21,9 @@ function saveOptions(opt) end filename = fullfile(optionDir, ['options', ... - taskString, ... - '_date-' datestr(now, 'yyyymmddHHMM'), ... - '.json']); + taskString, ... + '_date-' datestr(now, 'yyyymmddHHMM'), ... + '.json']); jsonFormat.indent = ' '; spm_jsonwrite(filename, opt, jsonFormat); diff --git a/src/workflows/bidsLesionSegmentation.m b/src/workflows/bidsLesionSegmentation.m index c405aef9..421a0fb0 100755 --- a/src/workflows/bidsLesionSegmentation.m +++ b/src/workflows/bidsLesionSegmentation.m @@ -30,5 +30,4 @@ function bidsLesionSegmentation(opt) end -end - +end diff --git a/tests/dummyData/derivatives/cpp_spm/sub-01/ses-01/anat/sub-01_ses-01_T1w.json b/tests/dummyData/derivatives/cpp_spm/sub-01/ses-01/anat/sub-01_ses-01_T1w.json deleted file mode 100755 index b2241835..00000000 --- a/tests/dummyData/derivatives/cpp_spm/sub-01/ses-01/anat/sub-01_ses-01_T1w.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Modality": "MR", - "RepetitionTime": 2.3, - "PhaseEncodingDirection": "j-", - "EchoTime": 0.00226, - "InversionTime": 0.9, - "SliceThickness": 1, - "FlipAngle": 8 -} diff --git a/tests/test_unit_converToValidCamelCase.m b/tests/test_unit_converToValidCamelCase.m index c134f68c..3ddf5883 100644 --- a/tests/test_unit_converToValidCamelCase.m +++ b/tests/test_unit_converToValidCamelCase.m @@ -1,23 +1,23 @@ -% (C) Copyright 2020 CPP_BIDS developers +% (C) Copyright 2021 CPP BIDS SPM-pipeline developers function test_suite = test_unit_converToValidCamelCase %#ok<*STOUT> - 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; + 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_converToValidCamelCaseBasic() - str = 'foo bar'; - str = converToValidCamelCase(str); - assertEqual(str, 'fooBar'); - - %% set up + str = 'foo bar'; + str = converToValidCamelCase(str); + assertEqual(str, 'fooBar'); - str = '&|@#-_(!{})01[]%+/=:;.?,\<> visual task'; - str = converToValidCamelCase(str); - assertEqual(str, '01VisualTask'); + %% set up + + str = '&|@#-_(!{})01[]%+/=:;.?,\<> visual task'; + str = converToValidCamelCase(str); + assertEqual(str, '01VisualTask'); end diff --git a/tests/test_unit_getFuncVoxelDims.m b/tests/test_unit_getFuncVoxelDims.m index 0527b2a4..2e70c5d5 100644 --- a/tests/test_unit_getFuncVoxelDims.m +++ b/tests/test_unit_getFuncVoxelDims.m @@ -13,7 +13,7 @@ function test_getFuncVoxelDimsBasic() opt.funcVoxelDims = []; subFuncDataDir = fullfile(fileparts(mfilename('fullpath')), '..', 'demos', ... - 'MoAE', 'output', 'MoAEpilot', 'sub-01', 'func'); + 'MoAE', 'inputs', 'raw', 'sub-01', 'func'); prefix = ''; diff --git a/tests/utils/setOptions.m b/tests/utils/setOptions.m index 2204c340..27a05a8b 100644 --- a/tests/utils/setOptions.m +++ b/tests/utils/setOptions.m @@ -8,7 +8,7 @@ opt.dataDir = fullfile(thisDir, ... '..', '..', 'demos', 'MoAE', 'inputs', 'raw'); - opt.model.file = fullfile((thisDir, ... + opt.model.file = fullfile(thisDir, ... '..', '..', 'demos', 'MoAE', 'models', 'model-MoAE_smdl.json'); opt.taskName = 'auditory'; @@ -16,7 +16,7 @@ else opt.taskName = task; - opt.derivativesDir = fullfile((thisDir, '..', 'dummyData'); + opt.derivativesDir = fullfile(thisDir, '..', 'dummyData'); opt.model.file = fullfile(opt.derivativesDir, 'models', ... ['model-' task '_smdl.json']); From 472e11c0ec5281c1545d620bb337179b1b61e8f1 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 19 Mar 2021 19:19:51 +0100 Subject: [PATCH 070/145] update gitignore --- .gitignore | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 63575b8a..1e64fe66 100644 --- a/.gitignore +++ b/.gitignore @@ -19,15 +19,10 @@ demos/*/inputs/ demos/*/cfg/*.json # test folder and dummy data -tests/cfg/*.json -tests/sub-01/* +tests/*.png tests/group/* -tests/models/*.json -tests/dummyData/derivatives/cpp_spm/sub-*/*/*/*.nii* -tests/dummyData/derivatives/cpp_spm/sub-*/*/*/*.tsv -tests/dummyData/derivatives/cpp_spm/sub-*/*/*/*.txt -tests/dummyData/derivatives/cpp_spm/sub-*/*/*/*.json -tests/dummyData/derivatives/cpp_spm/sub-*/stats/*/*/*.nii* +tests/*/*.json +tests/dummyData/derivatives/cpp_spm/sub-*/ # ignore content of the build folder of the doc docs/build/* From 09d55c61b395203bea78ab10cf825733da4bf62c Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 19 Mar 2021 19:29:07 +0100 Subject: [PATCH 071/145] update run test github action --- .github/workflows/run_tests.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 6fbd99b0..3ad370d8 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -67,10 +67,11 @@ jobs: - name: Prepare data run: | - output_folder='demos/MoAE/output/' - mkdir $output_folder - curl http://www.fil.ion.ucl.ac.uk/spm/download/data/MoAEpilot/MoAEpilot.bids.zip --output $output_folder'MoAEpilot.zip' - unzip $output_folder'MoAEpilot.zip' -d $output_folder + inputs_folder='demos/MoAE/inputs/' + mkdir $inputs_folder + curl http://www.fil.ion.ucl.ac.uk/spm/download/data/MoAEpilot/MoAEpilot.bids.zip --output $inputs_folder'MoAEpilot.zip' + unzip $inputs_folder'MoAEpilot.zip' -d $inputs_folder + mv $inputs_folder/MoAEpilot $inputs_folder/raw cd tests sh createDummyDataSet.sh cd .. From 6e95fb5fca4a593c9fd9b318261a1385003b2484 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 19 Mar 2021 19:43:47 +0100 Subject: [PATCH 072/145] add CPP_ROI as submodule --- .gitmodules | 3 +++ initCppSpm.m | 2 ++ lib/CPP_ROI | 1 + 3 files changed, 6 insertions(+) create mode 160000 lib/CPP_ROI diff --git a/.gitmodules b/.gitmodules index 4fd1373b..483bea38 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/spmup"] path = lib/spmup url = https://github.com/CPernet/spmup.git +[submodule "lib/CPP_ROI"] + path = lib/CPP_ROI + url = https://github.com/cpp-lln-lab/CPP_ROI.git diff --git a/initCppSpm.m b/initCppSpm.m index c1d02232..50186297 100644 --- a/initCppSpm.m +++ b/initCppSpm.m @@ -18,6 +18,8 @@ function initCppSpm() addpath(fullfile(thisDirectory, 'lib', 'bids-matlab')); + run(fullfile(thisDirectory, 'lib', 'CPP_ROI', 'initCppRoi')) + checkDependencies(); printCredits(); diff --git a/lib/CPP_ROI b/lib/CPP_ROI new file mode 160000 index 00000000..0f76a7fd --- /dev/null +++ b/lib/CPP_ROI @@ -0,0 +1 @@ +Subproject commit 0f76a7fdc9d1f0b44d835b4698c7e75b98e061f6 From 77a568f5ed4c9928dcb195f8cc52422aae0fa467 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 19 Mar 2021 20:20:27 +0100 Subject: [PATCH 073/145] update roi demo --- lib/CPP_ROI | 2 +- src/batches/setBatchReslice.m | 19 ++++++++----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/lib/CPP_ROI b/lib/CPP_ROI index 0f76a7fd..dd1724c4 160000 --- a/lib/CPP_ROI +++ b/lib/CPP_ROI @@ -1 +1 @@ -Subproject commit 0f76a7fdc9d1f0b44d835b4698c7e75b98e061f6 +Subproject commit dd1724c4fdda8371ba9b0044c34348d03d8f768e diff --git a/src/batches/setBatchReslice.m b/src/batches/setBatchReslice.m index cda6fa0b..dd08ea6e 100644 --- a/src/batches/setBatchReslice.m +++ b/src/batches/setBatchReslice.m @@ -24,21 +24,18 @@ if nargin < 4 || isempty(interp) interp = 4; end - - matlabbatch{end + 1}.spm.spatial.coreg.write.roptions.interp = interp; + write.roptions.interp = interp; if ischar(referenceImg) - matlabbatch{end}.spm.spatial.coreg.write.ref = {referenceImg}; - - else - matlabbatch{end}.spm.spatial.coreg.write.ref(1) = referenceImg; + referenceImg = {referenceImg}; end + write.ref(1) = referenceImg; - if iscell(sourceImages) - matlabbatch{end}.spm.spatial.coreg.write.source = sourceImages; - - else - matlabbatch{end}.spm.spatial.coreg.write.source(1) = referenceImg; + if ischar(sourceImages) + sourceImages = {sourceImages}; end + write.source(1) = sourceImages; + + matlabbatch{end + 1}.spm.spatial.coreg.write = write; end From a85d4dd20911094bd5d3a2c96032a4d37185eeda Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 20 Mar 2021 09:55:28 +0100 Subject: [PATCH 074/145] update doc setBatchReslice --- src/batches/setBatchReslice.m | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/batches/setBatchReslice.m b/src/batches/setBatchReslice.m index dd08ea6e..cb6fe775 100644 --- a/src/batches/setBatchReslice.m +++ b/src/batches/setBatchReslice.m @@ -2,18 +2,18 @@ function matlabbatch = setBatchReslice(matlabbatch, referenceImg, sourceImages, interp) % - % Set the batch for reslicing source images into the reference image??? + % Set the batch for reslicing source images to the reference image resolution % % USAGE:: % - % matlabbatch = setBatchReslice(matlabbatch, referenceImg, sourceImages) + % matlabbatch = setBatchReslice(matlabbatch, referenceImg, sourceImages, interp = 4) % % :param matlabbatch: list of SPM batches % :type matlabbatch: structure % :param referenceImg: Reference image - % :type referenceImg: string + % :type referenceImg: string or cellstring % :param sourceImages: Source images - % :type sourceImages: cell + % :type sourceImages: string or cellstring % % % :returns: - :matlabbatch: (structure) The matlabbatch ready to run the spm job From 9dc0e8956ebd7d8b7ed3e54b665c38f2940a8eef Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 20 Mar 2021 09:55:54 +0100 Subject: [PATCH 075/145] add function to remove SPM prefix --- src/utils/removeSpmPrefix.m | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/utils/removeSpmPrefix.m diff --git a/src/utils/removeSpmPrefix.m b/src/utils/removeSpmPrefix.m new file mode 100644 index 00000000..0f145523 --- /dev/null +++ b/src/utils/removeSpmPrefix.m @@ -0,0 +1,8 @@ +function image = removeSpmPrefix(image, prefix) + + basename = spm_file(image, 'basename'); + tmp = spm_file(image, 'basename', basename(length(prefix)+1:end)); + movefile(image, tmp); + image = tmp; + +end \ No newline at end of file From d710ccd2c6e9046f18f17d6ab73392eb6b3fc396 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 20 Mar 2021 09:56:11 +0100 Subject: [PATCH 076/145] update print credits --- src/utils/printCredits.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/printCredits.m b/src/utils/printCredits.m index 0e67fcd2..5e3cb4d7 100644 --- a/src/utils/printCredits.m +++ b/src/utils/printCredits.m @@ -17,7 +17,7 @@ function printCredits() DOI_URL = 'https://doi.org/10.5281/zenodo.3554331.'; - repoURL = 'https://github.com/cpp-lln-lab/CPP_BIDS_SPM_pipeline'; + repoURL = 'https://github.com/cpp-lln-lab/CPP_SPM'; disp('___________________________________________________________________________'); disp('___________________________________________________________________________'); @@ -28,7 +28,7 @@ function printCredits() disp(' \__)(__) (__) |___||_/ \_||__)'); disp(' '); - splash = 'Thank you for using the CPP lap pipeline - version %s. '; + splash = 'Thank you for using CPP SPM - version %s. '; fprintf(splash, versionNumber); fprintf('\n\n'); From c04fa9ffcb4d7e51eee849c1d6c2d433d03fa42d Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 20 Mar 2021 09:56:33 +0100 Subject: [PATCH 077/145] update initCppSpm --- initCppSpm.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/initCppSpm.m b/initCppSpm.m index 50186297..eda2dcad 100644 --- a/initCppSpm.m +++ b/initCppSpm.m @@ -18,11 +18,11 @@ function initCppSpm() addpath(fullfile(thisDirectory, 'lib', 'bids-matlab')); - run(fullfile(thisDirectory, 'lib', 'CPP_ROI', 'initCppRoi')) - checkDependencies(); - + printCredits(); + + run(fullfile(thisDirectory, 'lib', 'CPP_ROI', 'initCppRoi')) CPP_SPM_INITIALIZED = true(); From 26edb936b62b8a7551913e65ac482650b0119061 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 20 Mar 2021 09:57:13 +0100 Subject: [PATCH 078/145] remove extra mkdir to create dummy test data --- tests/createDummyDataSet.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/createDummyDataSet.sh b/tests/createDummyDataSet.sh index b841a19e..c85b10f4 100755 --- a/tests/createDummyDataSet.sh +++ b/tests/createDummyDataSet.sh @@ -4,7 +4,6 @@ # defines where the BIDS data set will be created StartDir=`pwd` # relative to starting directory -StartDir=$StartDir/dummyData/derivatives/cpp_spm mkdir $StartDir SubList='ctrl01 ctrl02 blind01 blind02 01 02' # subject list From 1e80183e15574eace31822c0cadfc6e77e8d5341 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 20 Mar 2021 09:57:43 +0100 Subject: [PATCH 079/145] update MoAE demo --- demos/MoAE/MoAEpilot_getOption.m | 1 + demos/MoAE/MoAEpilot_run.m | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/demos/MoAE/MoAEpilot_getOption.m b/demos/MoAE/MoAEpilot_getOption.m index 510b569e..399c4eef 100644 --- a/demos/MoAE/MoAEpilot_getOption.m +++ b/demos/MoAE/MoAEpilot_getOption.m @@ -11,6 +11,7 @@ % The directory where the data are located opt.dataDir = fullfile(fileparts(mfilename('fullpath')), 'inputs', 'raw'); + opt.derivativesDir = fullfile(opt.dataDir, '..', '..', 'outputs'); % Uncomment the lines below to run preprocessing % - don't use realign and unwarp diff --git a/demos/MoAE/MoAEpilot_run.m b/demos/MoAE/MoAEpilot_run.m index 2ffa2007..443a0f56 100644 --- a/demos/MoAE/MoAEpilot_run.m +++ b/demos/MoAE/MoAEpilot_run.m @@ -1,10 +1,11 @@ % (C) Copyright 2019 Remi Gau -% This script will download the dataset from the FIL for the block design SPM -% tutorial and will run the basic preprocessing, FFX and contrasts on it. +% This script will download the dataset from the FIL for the block design SPM tutorial +% and will run the basic preprocessing, FFX and contrasts on it. +% % Results might be a bit different from those in the manual as some -% default options are slightly different in this pipeline (e.g use of FAST -% instead of AR(1), motion regressors added) +% default options are slightly different in this pipeline +% (e.g use of FAST instead of AR(1), motion regressors added) clear; clc; From 36f9855944131337ab08dbc1b5ec3a16f907e092 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 21 Mar 2021 09:42:23 +0100 Subject: [PATCH 080/145] update README --- README.md | 80 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 4f90d642..0be2a07f 100644 --- a/README.md +++ b/README.md @@ -21,16 +21,69 @@ **Contributors** + [![All Contributors](https://img.shields.io/badge/all_contributors-7-orange.svg?style=flat-square)](#contributors-) + # CPP SPM -This is a set of functions to fMRI analysis on a +This is a set of functions to MRI analysis on a [BIDS data set](https://bids.neuroimaging.io/) using SPM12. +## Installation + +TODO + +We strongly recommend using the CPP fMRI analysis template repository (INSERT +URL) to use CPP_SPM. + +Download this repository and unzip the content where you want to install it. + +Or clone the repo. + +```bash +git clone https://github.com/cpp-lln-lab/CPP_SPM.git +``` + +Fire up Octave or Matlab and type + +```matlab + +cd CPP_SPM + +% Th following adds the relevant folders to your path. +% This needs to be done once per session (your path will not be saved) + +initCppSpm + +``` + +Please see our +[documentation](https://cpp-bids-spm.readthedocs.io/en/latest/index.html) for +more detailed instructions. + +### Dependencies + +Make sure that the following toolboxes are installed and added to the matlab +path. + +For instructions see the following links: + + + +| Dependencies | Used version | +| ---------------------------------------------------------- | ------------ | +| [Matlab](https://www.mathworks.com/products/matlab.html) | 20??? | +| or [octave](https://www.gnu.org/software/octave/) | 4.? | +| [SPM12](https://www.fil.ion.ucl.ac.uk/spm/software/spm12/) | v7487 | + + + +## Features + This can perform: - slice timing correction, @@ -55,33 +108,12 @@ This can perform: - GLM at the group level à la SPM (meaning using a summary statistics approach). +The core functions are in the `src` folder. + Please see our [documentation](https://cpp-bids-spm.readthedocs.io/en/latest/index.html) for more info. -The core functions are in the `src` folder. - -## Installation - -### Dependencies - -Make sure that the following toolboxes are installed and added to the matlab -path. - -For instructions see the following links: - - - -| Dependencies | Used version | -| ----------------------------------------------------------------------------------------- | ------------ | -| [Matlab](https://www.mathworks.com/products/matlab.html) | 20??? | -| or [octave](https://www.gnu.org/software/octave/) | 4.? | -| [SPM12](https://www.fil.ion.ucl.ac.uk/spm/software/spm12/) | v7487 | -| [Tools for NIfTI and ANALYZE image toolbox](https://github.com/sergivalverde/nifti_tools) | NA | -| [spmup](https://github.com/CPernet/spmup) | NA | - - - ## Contributing Feel free to open issues to report a bug and ask for improvements. From e338f6061ca38c12d7851dbef3742b012ec00062 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 21 Mar 2021 19:28:59 +0100 Subject: [PATCH 081/145] update submodule --- .gitignore | 1 - lib/CPP_ROI | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 1e64fe66..fadce1c0 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ options_task-*date*.json onsets*_events.mat # files in the demo folder related to running the demo analysis -demos/*/*.zip demos/*/outputs/ demos/*/inputs/ demos/*/cfg/*.json diff --git a/lib/CPP_ROI b/lib/CPP_ROI index dd1724c4..045e0e59 160000 --- a/lib/CPP_ROI +++ b/lib/CPP_ROI @@ -1 +1 @@ -Subproject commit dd1724c4fdda8371ba9b0044c34348d03d8f768e +Subproject commit 045e0e590c18a1f370041a45365d10ed49f86af7 From 25863da4955bb25086a1d1c12b7c465330a518ca Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 21 Mar 2021 19:58:30 +0100 Subject: [PATCH 082/145] update submodule --- lib/CPP_ROI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/CPP_ROI b/lib/CPP_ROI index 045e0e59..1bfd8a6f 160000 --- a/lib/CPP_ROI +++ b/lib/CPP_ROI @@ -1 +1 @@ -Subproject commit 045e0e590c18a1f370041a45365d10ed49f86af7 +Subproject commit 1bfd8a6f3e12b6f5b15fa1a8d98acccdc91f2d6c From 1af40c3d0635e0d7bef0ade8184c61209035dc6a Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 21 Mar 2021 20:10:58 +0100 Subject: [PATCH 083/145] fix script creation dummy data --- tests/createDummyDataSet.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/createDummyDataSet.sh b/tests/createDummyDataSet.sh index c85b10f4..5b7bc3f7 100755 --- a/tests/createDummyDataSet.sh +++ b/tests/createDummyDataSet.sh @@ -4,7 +4,7 @@ # defines where the BIDS data set will be created StartDir=`pwd` # relative to starting directory -mkdir $StartDir +StartDir=$StartDir/dummyData/derivatives/cpp_spm SubList='ctrl01 ctrl02 blind01 blind02 01 02' # subject list SesList='01 02' # session list @@ -97,7 +97,7 @@ do ThisDir=$StartDir/sub-$Subject/stats/task-vismotion_space-MNI_FWHM-6 mkdir $ThisDir - cp dummyData/SPM.mat $ThisDir + cp dummyData/SPM.mat $ThisDir/SPM.mat touch $ThisDir/mask.nii From 3ed5e4cb71066e5f06a59cb22d788376586672ac Mon Sep 17 00:00:00 2001 From: Michele MacLean Date: Mon, 15 Mar 2021 18:23:41 -0400 Subject: [PATCH 084/145] turn batches into functions --- .../setBatchLesionAbnormalitiesDetection.m | 48 ++++++++++++++----- src/batches/setBatchLesionOverlapMap.m | 23 +++++++-- src/batches/setBatchLesionSegmentation.m | 4 +- 3 files changed, 56 insertions(+), 19 deletions(-) diff --git a/src/batches/setBatchLesionAbnormalitiesDetection.m b/src/batches/setBatchLesionAbnormalitiesDetection.m index 337afe71..8e61ef21 100755 --- a/src/batches/setBatchLesionAbnormalitiesDetection.m +++ b/src/batches/setBatchLesionAbnormalitiesDetection.m @@ -1,14 +1,38 @@ -%----------------------------------------------------------------------- -% Job saved on 08-Mar-2021 12:47:16 by cfg_util (rev $Rev: 7345 $) -% spm SPM - SPM12 (7771) -% cfg_basicio BasicIO - Unknown -%----------------------------------------------------------------------- -matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3patients = ''; -matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3controls = ''; -matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3Alpha = 0.5; -matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3Lambda = -4; +% (C) Copyright 2021 CPP BIDS SPM-pipeline developers + + +function matlabbatch = setBatchLesionAbnormalities(matlabbatch, BIDS, opt, subLabel) + % + % Creates a batch to detect lesion abnormalities + % + % USAGE:: + % + % matlabbatch = setBatchLesionAbnormalities(matlabbatch, BIDS, opt, subLabel) + % + % :param matlabbatch: list of SPM batches + % :type matlabbatch: structure + % + % :returns: - :matlabbatch: (structure) + + printBatchName('Lesion abnormalities'); + + +matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3patients = ''; % patients: volumes, smoothed segmented images of patients +matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3controls = ''; % controls: volumes, smoothed segmented images of neurotypical controls +matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3Alpha = 0.5; % alpha parameter +matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3Lambda = -4; % lambda parameter + +% define SPM folder spmDir = spm('dir'); + +% specify lesion mask lesionMask = fullfile(spmDir, 'toolbox', 'ALI', 'Mask_image', 'mask_controls_vox2mm.nii'); -matlabbatch{1}.spm.tools.ali.outliers_detection.step3mask_thr = 0; -matlabbatch{1}.spm.tools.ali.outliers_detection.step3binary_thr = 0.3; -matlabbatch{1}.spm.tools.ali.outliers_detection.step3binary_size = 0.8; + + +outliers_detection.step3mask_thr = 0; % threshold for the mask +outliers_detection.step3binary_thr = 0.3; % binary lesion: threshold U +outliers_detection.step3binary_size = 0.8; % binary lesion: minimum size (in cm3) + +matlabbatch{end+1}.spm.tools.ali.outliers_detection = outliers_detection; + +end \ No newline at end of file diff --git a/src/batches/setBatchLesionOverlapMap.m b/src/batches/setBatchLesionOverlapMap.m index 9cde37ad..bea3a116 100755 --- a/src/batches/setBatchLesionOverlapMap.m +++ b/src/batches/setBatchLesionOverlapMap.m @@ -1,6 +1,19 @@ -%----------------------------------------------------------------------- -% Job saved on 08-Mar-2021 12:48:14 by cfg_util (rev $Rev: 7345 $) -% spm SPM - SPM12 (7771) -% cfg_basicio BasicIO - Unknown -%----------------------------------------------------------------------- +% (C) Copyright 2021 CPP BIDS SPM-pipeline developers + +function matlabbatch = setBatchLesionOverlapMap(matlabbatch, BIDS, opt, subLabel) + % + % Creates a batch for the lesion overlap map + % + % USAGE:: + % + % matlabbatch = setBatchLesionOverlapMap(matlabbatch, BIDS, opt, subLabel) + % + % :param matlabbatch: list of SPM batches + % :type matlabbatch: structure + % + % :returns: - :matlabbatch: (structure) + + printBatchName('Lesion overlap map'); + +% Specify lesion overlap map matlabbatch{1}.spm.tools.ali.lesion_overlap.lom = ''; diff --git a/src/batches/setBatchLesionSegmentation.m b/src/batches/setBatchLesionSegmentation.m index 2e519e39..f1478caf 100755 --- a/src/batches/setBatchLesionSegmentation.m +++ b/src/batches/setBatchLesionSegmentation.m @@ -1,6 +1,6 @@ % (C) Copyright 2021 CPP BIDS SPM-pipeline developers -function matlabbatch = setBatchLesionSegmentation(matlabbatch, BIDS, opt, subID) +function matlabbatch = setBatchLesionSegmentation(matlabbatch, BIDS, opt, subLabel) % % Creates a batch to segment the anatomical image for lesion detection % @@ -16,7 +16,7 @@ printBatchName('Lesion segmentation'); % find anatomical file -[anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); +[anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt); matlabbatch{1}.spm.tools.ali.unified_segmentation.step1data = [anatImage, anatDataDir]; % define SPM folder From 93f6cc01ee78236578c0b4baf26cce3ed9641769 Mon Sep 17 00:00:00 2001 From: Michele MacLean Date: Tue, 16 Mar 2021 11:50:18 -0400 Subject: [PATCH 085/145] add batchLesion script --- demos/lesiondetection/batchLesion.m | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 demos/lesiondetection/batchLesion.m diff --git a/demos/lesiondetection/batchLesion.m b/demos/lesiondetection/batchLesion.m new file mode 100644 index 00000000..84077d80 --- /dev/null +++ b/demos/lesiondetection/batchLesion.m @@ -0,0 +1,36 @@ +% (C) Copyright 2021 CPP BIDS SPM-pipeline developers + +clear; +clc; + +% URL of the data set to download +% https://gin.g-node.org/mwmaclean/CVI-Datalad/src/master/data +% URL = + +% Directory with this script becomes the current directory (CPP_SPM_dir) +pth = fileparts(mfilename('fullpath')); + +% We add all the subfunctions that are in the sub directories (CPP_SPM_dir) +addpath(genpath(fullfile(pth, '..', '..', 'src'))); + +% +initCppSpm() + +%% Set options +opt.taskName = 'rest'; + +%% Get Data +opt.dataDir = **path_to_your_BIDS_data**; +opt = checkOptions(opt); +checkDependencies(); + +%% Run batches +bidsLesionSegmentation(opt); + + + + + + + + From 3967a91f62404c89f104a7fdf75f7394b1a41f09 Mon Sep 17 00:00:00 2001 From: Michele MacLean Date: Tue, 16 Mar 2021 12:25:17 -0400 Subject: [PATCH 086/145] add getOption for lesion --- demos/lesiondetection/Lesion_getOption.m | 37 ++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 demos/lesiondetection/Lesion_getOption.m diff --git a/demos/lesiondetection/Lesion_getOption.m b/demos/lesiondetection/Lesion_getOption.m new file mode 100644 index 00000000..063f178f --- /dev/null +++ b/demos/lesiondetection/Lesion_getOption.m @@ -0,0 +1,37 @@ +% (C) Copyright 2021 CPP BIDS SPM-pipeline developers + +function opt = Lesion_getOption() + % + % Returns a structure that contains the options chosen by the user to run the source processing + % batch workflow + % + % USAGE:: + % + % opt = Lesion_getOption() + % + % :returns: - :optSource: (struct) + + if nargin < 1 + opt = []; + end + + % task to analyze + opt.taskName = 'rest'; + + % define group options + opt.groups = {'control', 'blind'}; + + % The directory where the data are located + opt.dataDir = fullfile(fileparts(mfilename('fullpath')), 'output', '...'); + opt.derivativesDir = fullfile(fileparts(mfilename('fullpath'))); + + % specify the result to compute + + + %% DO NOT TOUCH + opt = checkOptions(opt); + saveOptions(opt); + + +end + From 8a472b1084e503885262d8f3d98b9276d13cda93 Mon Sep 17 00:00:00 2001 From: Michele MacLean Date: Tue, 16 Mar 2021 13:42:26 -0400 Subject: [PATCH 087/145] add group loop --- demos/lesiondetection/batchLesion.m | 2 +- src/workflows/bidsLesionSegmentation.m | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/demos/lesiondetection/batchLesion.m b/demos/lesiondetection/batchLesion.m index 84077d80..0b239cb3 100644 --- a/demos/lesiondetection/batchLesion.m +++ b/demos/lesiondetection/batchLesion.m @@ -20,7 +20,7 @@ opt.taskName = 'rest'; %% Get Data -opt.dataDir = **path_to_your_BIDS_data**; +opt.dataDir = **path_to_your_BIDS_data**; %todo opt = checkOptions(opt); checkDependencies(); diff --git a/src/workflows/bidsLesionSegmentation.m b/src/workflows/bidsLesionSegmentation.m index 421a0fb0..ca721f47 100755 --- a/src/workflows/bidsLesionSegmentation.m +++ b/src/workflows/bidsLesionSegmentation.m @@ -16,6 +16,23 @@ function bidsLesionSegmentation(opt) % [BIDS, opt] = setUpWorkflow(opt, 'lesion segmentation'); + +% % % Loop through the groups, subjects, and sessions +% % for iGroup = 1:length(group) +% % +% % groupName = group(iGroup).name; +% % +% % for iSub = 1:group(iGroup).numSub +% % +% % subID = group(iGroup).subNumber{iSub}; +% % +% % printProcessingSubject(groupName, iSub, subID); +% % +% % [meanImage, meanFuncDir] = getMeanFuncFilename(BIDS, subID, opt); +% % +% % get grey and white matter and CSF tissue probability maps +% % [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); +% % TPMs = validationInputFile(anatDataDir, anatImage, 'c[123]'); parfor iSub = 1:numel(opt.subjects) From 529dce8f07b1769780eb8dd7ec6889df8544927a Mon Sep 17 00:00:00 2001 From: Michele MacLean Date: Tue, 16 Mar 2021 17:43:42 -0400 Subject: [PATCH 088/145] auto-fix style --- demos/lesiondetection/Lesion_getOption.m | 7 +--- demos/lesiondetection/batchLesion.m | 15 ++------ .../setBatchLesionAbnormalitiesDetection.m | 31 ++++++++------- src/batches/setBatchLesionOverlapMap.m | 4 +- src/batches/setBatchLesionSegmentation.m | 35 ++++++++--------- src/workflows/bidsLesionSegmentation.m | 38 ++++++++++--------- 6 files changed, 63 insertions(+), 67 deletions(-) diff --git a/demos/lesiondetection/Lesion_getOption.m b/demos/lesiondetection/Lesion_getOption.m index 063f178f..1408344c 100644 --- a/demos/lesiondetection/Lesion_getOption.m +++ b/demos/lesiondetection/Lesion_getOption.m @@ -17,21 +17,18 @@ % task to analyze opt.taskName = 'rest'; - + % define group options opt.groups = {'control', 'blind'}; % The directory where the data are located opt.dataDir = fullfile(fileparts(mfilename('fullpath')), 'output', '...'); opt.derivativesDir = fullfile(fileparts(mfilename('fullpath'))); - - % specify the result to compute + % specify the result to compute %% DO NOT TOUCH opt = checkOptions(opt); saveOptions(opt); - end - diff --git a/demos/lesiondetection/batchLesion.m b/demos/lesiondetection/batchLesion.m index 0b239cb3..275897ef 100644 --- a/demos/lesiondetection/batchLesion.m +++ b/demos/lesiondetection/batchLesion.m @@ -4,8 +4,7 @@ clc; % URL of the data set to download -% https://gin.g-node.org/mwmaclean/CVI-Datalad/src/master/data -% URL = +% URL = https://gin.g-node.org/mwmaclean/CVI-Datalad/src/master/data % Directory with this script becomes the current directory (CPP_SPM_dir) pth = fileparts(mfilename('fullpath')); @@ -14,23 +13,15 @@ addpath(genpath(fullfile(pth, '..', '..', 'src'))); % -initCppSpm() +initCppSpm(); %% Set options opt.taskName = 'rest'; %% Get Data -opt.dataDir = **path_to_your_BIDS_data**; %todo +opt.dataDir = path_to_your_BIDS_data; % todo opt = checkOptions(opt); checkDependencies(); %% Run batches bidsLesionSegmentation(opt); - - - - - - - - diff --git a/src/batches/setBatchLesionAbnormalitiesDetection.m b/src/batches/setBatchLesionAbnormalitiesDetection.m index 8e61ef21..cf906292 100755 --- a/src/batches/setBatchLesionAbnormalitiesDetection.m +++ b/src/batches/setBatchLesionAbnormalitiesDetection.m @@ -1,6 +1,5 @@ % (C) Copyright 2021 CPP BIDS SPM-pipeline developers - function matlabbatch = setBatchLesionAbnormalities(matlabbatch, BIDS, opt, subLabel) % % Creates a batch to detect lesion abnormalities @@ -16,23 +15,27 @@ printBatchName('Lesion abnormalities'); + % Defin smoothed segmented images of patients + matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3patients = ''; -matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3patients = ''; % patients: volumes, smoothed segmented images of patients -matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3controls = ''; % controls: volumes, smoothed segmented images of neurotypical controls -matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3Alpha = 0.5; % alpha parameter -matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3Lambda = -4; % lambda parameter + % Defin smoothed segmented images of neurotypical controls + matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3controls = ''; -% define SPM folder -spmDir = spm('dir'); + % Specify alpha parameter + matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3Alpha = 0.5; + % Specify lambda parameter + matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3Lambda = -4; -% specify lesion mask -lesionMask = fullfile(spmDir, 'toolbox', 'ALI', 'Mask_image', 'mask_controls_vox2mm.nii'); + % define SPM folder + spmDir = spm('dir'); + % specify lesion mask + lesionMask = fullfile(spmDir, 'toolbox', 'ALI', 'Mask_image', 'mask_controls_vox2mm.nii'); -outliers_detection.step3mask_thr = 0; % threshold for the mask -outliers_detection.step3binary_thr = 0.3; % binary lesion: threshold U -outliers_detection.step3binary_size = 0.8; % binary lesion: minimum size (in cm3) + outliers_detection.step3mask_thr = 0; % threshold for the mask + outliers_detection.step3binary_thr = 0.3; % binary lesion: threshold U + outliers_detection.step3binary_size = 0.8; % binary lesion: minimum size (in cm3) -matlabbatch{end+1}.spm.tools.ali.outliers_detection = outliers_detection; + matlabbatch{end + 1}.spm.tools.ali.outliers_detection = outliers_detection; -end \ No newline at end of file +end diff --git a/src/batches/setBatchLesionOverlapMap.m b/src/batches/setBatchLesionOverlapMap.m index bea3a116..01c7af5e 100755 --- a/src/batches/setBatchLesionOverlapMap.m +++ b/src/batches/setBatchLesionOverlapMap.m @@ -15,5 +15,5 @@ printBatchName('Lesion overlap map'); -% Specify lesion overlap map -matlabbatch{1}.spm.tools.ali.lesion_overlap.lom = ''; + % Specify lesion overlap map + matlabbatch{1}.spm.tools.ali.lesion_overlap.lom = ''; diff --git a/src/batches/setBatchLesionSegmentation.m b/src/batches/setBatchLesionSegmentation.m index f1478caf..bdd6f0e7 100755 --- a/src/batches/setBatchLesionSegmentation.m +++ b/src/batches/setBatchLesionSegmentation.m @@ -15,25 +15,26 @@ printBatchName('Lesion segmentation'); -% find anatomical file -[anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt); -matlabbatch{1}.spm.tools.ali.unified_segmentation.step1data = [anatImage, anatDataDir]; + % find anatomical file + [anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt); + matlabbatch{1}.spm.tools.ali.unified_segmentation.step1data = [anatImage, anatDataDir]; -% define SPM folder -spmDir = spm('dir'); + % define SPM folder + spmDir = spm('dir'); -% specify Prior EXTRA class (lesion prior map) -lesionPriorMap = fullfile(spmDir, 'toolbox', 'ALI', 'Priors_extraClass', 'wc4prior0.nii'); + % specify Prior EXTRA class (lesion prior map) + lesionPriorMap = fullfile(spmDir, 'toolbox', 'ALI', 'Priors_extraClass', 'wc4prior0.nii'); -unified_segmentation.step1prior = {lesionPriorMap}; -unified_segmentation.step1niti = 2; % number of iterations -unified_segmentation.step1thr_prob = 0.333333333333333; % threshold probability -unified_segmentation.step1thr_size = 0.8; % threshold size (in cm3) -unified_segmentation.step1coregister = 1; % coregister in MNI space (yes: 1) -unified_segmentation.step1mask = {''}; % specify cost function mask CFM (optional) -unified_segmentation.step1vox = 2; % Voxel sizes (in mm) -unified_segmentation.step1fwhm = [8 8 8]; % Smooth: FWHM + unified_segmentation.step1prior = {lesionPriorMap}; -matlabbatch{end+1}.spm.tools.ali.unified_segmentation = unified_segmentation; + unified_segmentation.step1niti = 2; % number of iterations + unified_segmentation.step1thr_prob = 0.333333333333333; % threshold probability + unified_segmentation.step1thr_size = 0.8; % threshold size (in cm3) + unified_segmentation.step1coregister = 1; % coregister in MNI space (yes: 1) + unified_segmentation.step1mask = {''}; % specify cost function mask(optional) + unified_segmentation.step1vox = 2; % Voxel sizes (in mm) + unified_segmentation.step1fwhm = [8 8 8]; % Smooth: FWHM -end \ No newline at end of file + matlabbatch{end + 1}.spm.tools.ali.unified_segmentation = unified_segmentation; + +end diff --git a/src/workflows/bidsLesionSegmentation.m b/src/workflows/bidsLesionSegmentation.m index ca721f47..52489e87 100755 --- a/src/workflows/bidsLesionSegmentation.m +++ b/src/workflows/bidsLesionSegmentation.m @@ -15,24 +15,28 @@ function bidsLesionSegmentation(opt) % Segmentation will be performed using the information provided in the BIDS data set. % + if nargin < 1 + opt = []; + end + [BIDS, opt] = setUpWorkflow(opt, 'lesion segmentation'); - -% % % Loop through the groups, subjects, and sessions -% % for iGroup = 1:length(group) -% % -% % groupName = group(iGroup).name; -% % -% % for iSub = 1:group(iGroup).numSub -% % -% % subID = group(iGroup).subNumber{iSub}; -% % -% % printProcessingSubject(groupName, iSub, subID); -% % -% % [meanImage, meanFuncDir] = getMeanFuncFilename(BIDS, subID, opt); -% % -% % get grey and white matter and CSF tissue probability maps -% % [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); -% % TPMs = validationInputFile(anatDataDir, anatImage, 'c[123]'); + + % % % Loop through the groups, subjects, and sessions + % % for iGroup = 1:length(group) + % % + % % groupName = group(iGroup).name; + % % + % % for iSub = 1:group(iGroup).numSub + % % + % % subID = group(iGroup).subNumber{iSub}; + % % + % % printProcessingSubject(groupName, iSub, subID); + % % + % % [meanImage, meanFuncDir] = getMeanFuncFilename(BIDS, subID, opt); + % % + % % get grey and white matter and CSF tissue probability maps + % % [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); + % % TPMs = validationInputFile(anatDataDir, anatImage, 'c[123]'); parfor iSub = 1:numel(opt.subjects) From 2392346e0acf2b494df5802be83ba82c94666462 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 22 Mar 2021 21:17:52 +0100 Subject: [PATCH 089/145] fix bidsLesionWorkflow --- demos/lesiondetection/Lesion_getOption.m | 15 +------------- demos/lesiondetection/batchLesion.m | 21 +++++++------------- src/batches/setBatchLesionSegmentation.m | 2 +- src/workflows/bidsLesionSegmentation.m | 25 ++---------------------- 4 files changed, 11 insertions(+), 52 deletions(-) diff --git a/demos/lesiondetection/Lesion_getOption.m b/demos/lesiondetection/Lesion_getOption.m index 1408344c..04e97c78 100644 --- a/demos/lesiondetection/Lesion_getOption.m +++ b/demos/lesiondetection/Lesion_getOption.m @@ -11,21 +11,8 @@ % % :returns: - :optSource: (struct) - if nargin < 1 - opt = []; - end - - % task to analyze - opt.taskName = 'rest'; - - % define group options - opt.groups = {'control', 'blind'}; - % The directory where the data are located - opt.dataDir = fullfile(fileparts(mfilename('fullpath')), 'output', '...'); - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath'))); - - % specify the result to compute + opt.dataDir = '/home/remi/gin/CVI-Datalad/data'; %% DO NOT TOUCH opt = checkOptions(opt); diff --git a/demos/lesiondetection/batchLesion.m b/demos/lesiondetection/batchLesion.m index 275897ef..7e66dc7c 100644 --- a/demos/lesiondetection/batchLesion.m +++ b/demos/lesiondetection/batchLesion.m @@ -6,22 +6,15 @@ % URL of the data set to download % URL = https://gin.g-node.org/mwmaclean/CVI-Datalad/src/master/data -% Directory with this script becomes the current directory (CPP_SPM_dir) -pth = fileparts(mfilename('fullpath')); - -% We add all the subfunctions that are in the sub directories (CPP_SPM_dir) -addpath(genpath(fullfile(pth, '..', '..', 'src'))); - -% -initCppSpm(); - -%% Set options -opt.taskName = 'rest'; +run ../../initCppSpm.m; %% Get Data -opt.dataDir = path_to_your_BIDS_data; % todo -opt = checkOptions(opt); -checkDependencies(); +opt = Lesion_getOption(); %% Run batches +reportBIDS(opt); + +deleteZippedNii = true; +bidsCopyRawFolder(opt, deleteZippedNii, {'anat'}); + bidsLesionSegmentation(opt); diff --git a/src/batches/setBatchLesionSegmentation.m b/src/batches/setBatchLesionSegmentation.m index bdd6f0e7..8a0a7e22 100755 --- a/src/batches/setBatchLesionSegmentation.m +++ b/src/batches/setBatchLesionSegmentation.m @@ -17,7 +17,7 @@ % find anatomical file [anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt); - matlabbatch{1}.spm.tools.ali.unified_segmentation.step1data = [anatImage, anatDataDir]; + unified_segmentation.step1data{1} = fullfile(anatDataDir, anatImage); % define SPM folder spmDir = spm('dir'); diff --git a/src/workflows/bidsLesionSegmentation.m b/src/workflows/bidsLesionSegmentation.m index 52489e87..ca0d9fde 100755 --- a/src/workflows/bidsLesionSegmentation.m +++ b/src/workflows/bidsLesionSegmentation.m @@ -15,37 +15,16 @@ function bidsLesionSegmentation(opt) % Segmentation will be performed using the information provided in the BIDS data set. % - if nargin < 1 - opt = []; - end - [BIDS, opt] = setUpWorkflow(opt, 'lesion segmentation'); - % % % Loop through the groups, subjects, and sessions - % % for iGroup = 1:length(group) - % % - % % groupName = group(iGroup).name; - % % - % % for iSub = 1:group(iGroup).numSub - % % - % % subID = group(iGroup).subNumber{iSub}; - % % - % % printProcessingSubject(groupName, iSub, subID); - % % - % % [meanImage, meanFuncDir] = getMeanFuncFilename(BIDS, subID, opt); - % % - % % get grey and white matter and CSF tissue probability maps - % % [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); - % % TPMs = validationInputFile(anatDataDir, anatImage, 'c[123]'); - - parfor iSub = 1:numel(opt.subjects) + for iSub = 1:numel(opt.subjects) subLabel = opt.subjects{iSub}; printProcessingSubject(iSub, subLabel); matlabbatch = []; - matlabbatch = setBatchSTC(matlabbatch, BIDS, opt, subLabel); + matlabbatch = setBatchLesionSegmentation(matlabbatch, BIDS, opt, subLabel); saveAndRunWorkflow(matlabbatch, 'LesionSegmentation', opt, subLabel); From 82ed2f0a725269e1d80f23eeab26246febc14994 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 22 Mar 2021 21:18:15 +0100 Subject: [PATCH 090/145] lint --- demos/MoAE/MoAEpilot_run.m | 4 ++-- initCppSpm.m | 6 +++--- src/batches/setBatchReslice.m | 2 +- src/utils/removeSpmPrefix.m | 14 +++++++------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/demos/MoAE/MoAEpilot_run.m b/demos/MoAE/MoAEpilot_run.m index 443a0f56..84f614c6 100644 --- a/demos/MoAE/MoAEpilot_run.m +++ b/demos/MoAE/MoAEpilot_run.m @@ -1,10 +1,10 @@ % (C) Copyright 2019 Remi Gau -% This script will download the dataset from the FIL for the block design SPM tutorial +% This script will download the dataset from the FIL for the block design SPM tutorial % and will run the basic preprocessing, FFX and contrasts on it. % % Results might be a bit different from those in the manual as some -% default options are slightly different in this pipeline +% default options are slightly different in this pipeline % (e.g use of FAST instead of AR(1), motion regressors added) clear; diff --git a/initCppSpm.m b/initCppSpm.m index eda2dcad..1497793d 100644 --- a/initCppSpm.m +++ b/initCppSpm.m @@ -19,10 +19,10 @@ function initCppSpm() addpath(fullfile(thisDirectory, 'lib', 'bids-matlab')); checkDependencies(); - + printCredits(); - - run(fullfile(thisDirectory, 'lib', 'CPP_ROI', 'initCppRoi')) + + run(fullfile(thisDirectory, 'lib', 'CPP_ROI', 'initCppRoi')); CPP_SPM_INITIALIZED = true(); diff --git a/src/batches/setBatchReslice.m b/src/batches/setBatchReslice.m index cb6fe775..2ff1c66f 100644 --- a/src/batches/setBatchReslice.m +++ b/src/batches/setBatchReslice.m @@ -35,7 +35,7 @@ sourceImages = {sourceImages}; end write.source(1) = sourceImages; - + matlabbatch{end + 1}.spm.spatial.coreg.write = write; end diff --git a/src/utils/removeSpmPrefix.m b/src/utils/removeSpmPrefix.m index 0f145523..9f6a654b 100644 --- a/src/utils/removeSpmPrefix.m +++ b/src/utils/removeSpmPrefix.m @@ -1,8 +1,8 @@ function image = removeSpmPrefix(image, prefix) - - basename = spm_file(image, 'basename'); - tmp = spm_file(image, 'basename', basename(length(prefix)+1:end)); - movefile(image, tmp); - image = tmp; - -end \ No newline at end of file + + basename = spm_file(image, 'basename'); + tmp = spm_file(image, 'basename', basename(length(prefix) + 1:end)); + movefile(image, tmp); + image = tmp; + +end From 870a1fc9b07af9a03c0ab801feab02988c0b6db9 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 22 Mar 2021 21:26:38 +0100 Subject: [PATCH 091/145] copy figures to subect dir after lesion workflow --- src/workflows/bidsLesionSegmentation.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/workflows/bidsLesionSegmentation.m b/src/workflows/bidsLesionSegmentation.m index ca0d9fde..627cba8e 100755 --- a/src/workflows/bidsLesionSegmentation.m +++ b/src/workflows/bidsLesionSegmentation.m @@ -28,6 +28,8 @@ function bidsLesionSegmentation(opt) saveAndRunWorkflow(matlabbatch, 'LesionSegmentation', opt, subLabel); + copyFigures(BIDS, opt, subLabel); + end end From 531c7e6fcea299bbbe5f0795ded49395f1fab9ab Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 22 Mar 2021 23:05:21 +0100 Subject: [PATCH 092/145] make sure GLM figures are saved --- src/batches/setBatchPrintFigure.m | 2 +- src/workflows/bidsFFX.m | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/batches/setBatchPrintFigure.m b/src/batches/setBatchPrintFigure.m index d7d5d3e7..fbdac056 100644 --- a/src/batches/setBatchPrintFigure.m +++ b/src/batches/setBatchPrintFigure.m @@ -15,7 +15,7 @@ % % :returns: - :matlabbatch: (structure) The matlabbatch ready to run the spm job - if ~spm('CmdLine', true) + if spm('CmdLine', true) printBatchName('print figure'); diff --git a/src/workflows/bidsFFX.m b/src/workflows/bidsFFX.m index 83eb8f0c..396fbde5 100644 --- a/src/workflows/bidsFFX.m +++ b/src/workflows/bidsFFX.m @@ -47,15 +47,19 @@ function bidsFFX(action, opt, funcFWHM) case 'specifyAndEstimate' matlabbatch = setBatchSubjectLevelGLMSpec(matlabbatch, BIDS, opt, subLabel, funcFWHM); - matlabbatch = setBatchPrintFigure(matlabbatch, [ ... - 'sub-', subLabel, ... - '_task-', opt.taskName, ... - '_design_before_estimation']); + matlabbatch = setBatchPrintFigure(matlabbatch, fullfile(getFFXdir(subLabel, ... + funcFWHM, ... + opt), ... + ['sub-', subLabel, ... + '_task-', opt.taskName, ... + '_design_before_estimation'])); matlabbatch = setBatchEstimateModel(matlabbatch, opt); - matlabbatch = setBatchPrintFigure(matlabbatch, [ ... - 'sub-', subLabel, ... - '_task-', opt.taskName, ... - '_design_after_estimation']); + matlabbatch = setBatchPrintFigure(matlabbatch, fullfile(getFFXdir(subLabel, ... + funcFWHM, ... + opt), ... + ['sub-', subLabel, ... + '_task-', opt.taskName, ... + '_design_after_estimation'])); batchName = ... ['specify_estimate_ffx_task-', opt.taskName, ... From ece5f1974dfdd2e76b827a735498de85d2e64677 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Tue, 23 Mar 2021 17:10:57 +0100 Subject: [PATCH 093/145] update CPP_ROI submodule --- lib/CPP_ROI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/CPP_ROI b/lib/CPP_ROI index 1bfd8a6f..3757d26c 160000 --- a/lib/CPP_ROI +++ b/lib/CPP_ROI @@ -1 +1 @@ -Subproject commit 1bfd8a6f3e12b6f5b15fa1a8d98acccdc91f2d6c +Subproject commit 3757d26cd61a6a4e62c790c76edadc581825f4cf From 7c911e6d0d97ace4f89b0c708a7cd297af0c260b Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 31 Mar 2021 10:07:15 +0200 Subject: [PATCH 094/145] rename GLM design matrix figure output --- src/workflows/bidsFFX.m | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/workflows/bidsFFX.m b/src/workflows/bidsFFX.m index 396fbde5..0d5385b4 100644 --- a/src/workflows/bidsFFX.m +++ b/src/workflows/bidsFFX.m @@ -45,21 +45,30 @@ function bidsFFX(action, opt, funcFWHM) switch action case 'specifyAndEstimate' - - matlabbatch = setBatchSubjectLevelGLMSpec(matlabbatch, BIDS, opt, subLabel, funcFWHM); + + matlabbatch = setBatchSubjectLevelGLMSpec(matlabbatch, BIDS, opt, subLabel, funcFWHM); + + p = struct(... + 'filename', '', ... + 'type', 'designmatrix', ... + 'ext', '.png', ... + 'sub', subLabel, ... + 'task', opt.taskName, ... + 'space', opt.space, ... + 'desc', 'before estimation'); + matlabbatch = setBatchPrintFigure(matlabbatch, fullfile(getFFXdir(subLabel, ... funcFWHM, ... opt), ... - ['sub-', subLabel, ... - '_task-', opt.taskName, ... - '_design_before_estimation'])); + createFilename(p))); + matlabbatch = setBatchEstimateModel(matlabbatch, opt); + + p.desc = 'after estimation'; matlabbatch = setBatchPrintFigure(matlabbatch, fullfile(getFFXdir(subLabel, ... funcFWHM, ... opt), ... - ['sub-', subLabel, ... - '_task-', opt.taskName, ... - '_design_after_estimation'])); + createFilename(p))); batchName = ... ['specify_estimate_ffx_task-', opt.taskName, ... From 931efdf6c339baede38f79def16624a3fd9c6231 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 31 Mar 2021 17:25:11 +0200 Subject: [PATCH 095/145] make name of onsets file more bids compliant --- lib/CPP_ROI | 2 +- src/subject_level/convertOnsetTsvToMat.m | 7 ++++++- src/workflows/bidsFFX.m | 1 - 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/CPP_ROI b/lib/CPP_ROI index 3757d26c..f1f7d5f0 160000 --- a/lib/CPP_ROI +++ b/lib/CPP_ROI @@ -1 +1 @@ -Subproject commit 3757d26cd61a6a4e62c790c76edadc581825f4cf +Subproject commit f1f7d5f0867d34b2ea26b60640fc3848f48d8b48 diff --git a/src/subject_level/convertOnsetTsvToMat.m b/src/subject_level/convertOnsetTsvToMat.m index c81cc80f..461d31e6 100644 --- a/src/subject_level/convertOnsetTsvToMat.m +++ b/src/subject_level/convertOnsetTsvToMat.m @@ -88,8 +88,13 @@ % save the onsets as a matfile [pth, file] = spm_fileparts(tsvFile); + + p = bids.internal.parse_filename(file); + p.space = opt.space; + p.type = 'onsets'; + p.ext = '.mat'; - fullpathOnsetFileName = fullfile(pth, ['onsets_' file '.mat']); + fullpathOnsetFileName = fullfile(pth, createFilename(p)); save(fullpathOnsetFileName, ... 'names', 'onsets', 'durations', ... diff --git a/src/workflows/bidsFFX.m b/src/workflows/bidsFFX.m index 0d5385b4..f9bc32b1 100644 --- a/src/workflows/bidsFFX.m +++ b/src/workflows/bidsFFX.m @@ -49,7 +49,6 @@ function bidsFFX(action, opt, funcFWHM) matlabbatch = setBatchSubjectLevelGLMSpec(matlabbatch, BIDS, opt, subLabel, funcFWHM); p = struct(... - 'filename', '', ... 'type', 'designmatrix', ... 'ext', '.png', ... 'sub', subLabel, ... From 20afd17f4eb898a9fa790e1d681cc5db10775069 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 1 Apr 2021 15:47:21 +0200 Subject: [PATCH 096/145] add demo to run pipeline with several ouput resolutions --- demos/spm_face_rep/FaceRep_getOption.m | 1 + demos/spm_face_rep/face_rep_resolution.m | 100 ++++++++++++++ .../models/model-faceRepetition_smdl.json | 130 ++++++++++++++++++ src/batches/setBatchReslice.m | 5 +- src/workflows/bidsResults.m | 6 +- 5 files changed, 239 insertions(+), 3 deletions(-) create mode 100644 demos/spm_face_rep/face_rep_resolution.m create mode 100644 demos/spm_face_rep/models/model-faceRepetition_smdl.json diff --git a/demos/spm_face_rep/FaceRep_getOption.m b/demos/spm_face_rep/FaceRep_getOption.m index 16886f77..2539378a 100644 --- a/demos/spm_face_rep/FaceRep_getOption.m +++ b/demos/spm_face_rep/FaceRep_getOption.m @@ -11,6 +11,7 @@ % The directory where the data are located opt.dataDir = fullfile(fileparts(mfilename('fullpath')), 'outputs', 'raw'); + opt.model.hrfDerivatives = [1 1]; %% DO NOT TOUCH diff --git a/demos/spm_face_rep/face_rep_resolution.m b/demos/spm_face_rep/face_rep_resolution.m new file mode 100644 index 00000000..ce53f18a --- /dev/null +++ b/demos/spm_face_rep/face_rep_resolution.m @@ -0,0 +1,100 @@ +% (C) Copyright 2019 Remi Gau + +% adapted from face_rep_fun +% +% runs preprocessing with different final spatial resolution in MNI space +% + +clear; +clc; + +FWHM = 6; + +downloadData = false; + +run ../../initCppSpm.m; + +%% Set options +opt = FaceRep_getOption(); +opt = FaceRep_getOptionResults(opt); + +%% Removes previous analysis, gets data and converts it to BIDS +if downloadData + + dowload_convert_face_rep_ds(); + +end + +%% Run batches + +reportBIDS(opt); + +for iResolution = 1:0.5:3 + + opt.funcVoxelDims = repmat(iResolution, 1, 3); + + opt.derivativesDir = spm_file( ... + fullfile(opt.dataDir, ... + '..', ... + 'derivatives', ... + ['cpp_spm-res' num2str(iResolution)]), 'cpath'); + + bidsCopyRawFolder(opt, 1); + + bidsSTC(opt); + + bidsSpatialPrepro(opt); + + bidsSmoothing(FWHM, opt); + + bidsFFX('specifyAndEstimate', opt, FWHM); + bidsFFX('contrasts', opt, FWHM); + + % specify underlay image + subLabel = '01'; + [BIDS, opt] = getData(opt); + [~, anatDataDir] = getAnatFilename(BIDS, subLabel, opt); + opt.result.Steps(1).Output.montage.background = spm_select('FPList', ... + anatDataDir, ... + '^wm.*.nii$'); + + bidsResults(opt, FWHM); + +end + +function opt = FaceRep_getOptionResults(opt) + + opt.model.file = fullfile( ... + fileparts(mfilename('fullpath')), ... + 'models', ... + 'model-faceRepetition_smdl.json'); + + opt.glmQA.do = false; + + % Specify the result to compute + opt.result.Steps(1) = returnDefaultResultsStructure(); + + opt.result.Steps(1).Level = 'subject'; + + opt.result.Steps(1).Contrasts(1).Name = 'faces_gt_baseline'; + + opt.result.Steps(1).Contrasts(1).MC = 'FWE'; + opt.result.Steps(1).Contrasts(1).p = 0.05; + opt.result.Steps(1).Contrasts(1).k = 5; + + % Specify how you want your output (all the following are on false by default) + opt.result.Steps(1).Output.png = true(); + + opt.result.Steps(1).Output.csv = true(); + + opt.result.Steps(1).Output.thresh_spm = true(); + + opt.result.Steps(1).Output.binary = true(); + + % MONTAGE FIGURE OPTIONS + opt.result.Steps(1).Output.montage.do = true(); + opt.result.Steps(1).Output.montage.slices = -26:3:6; % in mm + % axial is default 'sagittal', 'coronal' + opt.result.Steps(1).Output.montage.orientation = 'axial'; + +end diff --git a/demos/spm_face_rep/models/model-faceRepetition_smdl.json b/demos/spm_face_rep/models/model-faceRepetition_smdl.json new file mode 100644 index 00000000..45ddda67 --- /dev/null +++ b/demos/spm_face_rep/models/model-faceRepetition_smdl.json @@ -0,0 +1,130 @@ +{ + "Name": "face repetition resampling", + "Description": "model for face repetition to check resampling effects", + "Input": { + "task": "facerepetition" + }, + "Steps": [ + { + "Level": "subject", + "Transformations": [ + { + "Name": "Factor", + "Inputs": [ + "trial_type" + ] + }, + { + "Name": "Convolve", + "Model": "spm", + "Inputs": [ + " " + ] + } + ], + "Model": { + "X": [ + "trial_type.F1", + "trial_type.F2", + "trial_type.N1", + "trial_type.N2", + "trans_x", + "trans_y", + "trans_z", + "rot_x", + "rot_y", + "rot_z" + ], + "Options": { + "high_pass_filter_cutoff_secs": 128 + }, + "Software": { + "SPM": { + "whitening": "FAST" + } + }, + "Mask": " " + }, + "AutoContrasts": [ + "trial_type.F1", + "trial_type.F2", + "trial_type.N1", + "trial_type.N2" + ], + "Contrasts": [ + { + "Name": "faces_gt_baseline", + "ConditionList": [ + "trial_type.F1", + "trial_type.F2", + "trial_type.N1", + "trial_type.N2" + ], + "weights": [ + 1, + 1, + 1, + 1 + ], + "type": "t" + } + ] + }, + { + "Level": "run", + "Transformations": [ + { + "Name": "Factor", + "Inputs": [ + "trial_type" + ] + }, + { + "Name": "Convolve", + "Model": "spm", + "Inputs": [ + " " + ] + } + ], + "Model": { + "X": [ + "trial_type.F1", + "trial_type.F2", + "trial_type.N1", + "trial_type.N2", + "trans_x", + "trans_y", + "trans_z", + "rot_x", + "rot_y", + "rot_z" + ], + "Options": { + "high_pass_filter_cutoff_secs": 128 + }, + "Software": { + "SPM": { + "whitening": "FAST" + } + }, + "Mask": " " + }, + "AutoContrasts": [ + "trial_type.F1", + "trial_type.F2", + "trial_type.N1", + "trial_type.N2" + ] + }, + { + "Level": "dataset", + "AutoContrasts": [ + "trial_type.F1", + "trial_type.F2", + "trial_type.N1", + "trial_type.N2" + ] + } + ] +} \ No newline at end of file diff --git a/src/batches/setBatchReslice.m b/src/batches/setBatchReslice.m index 2ff1c66f..97a08d94 100644 --- a/src/batches/setBatchReslice.m +++ b/src/batches/setBatchReslice.m @@ -32,9 +32,10 @@ write.ref(1) = referenceImg; if ischar(sourceImages) - sourceImages = {sourceImages}; + write.source = {sourceImages}; + elseif iscell(sourceImages) + write.source = sourceImages; end - write.source(1) = sourceImages; matlabbatch{end + 1}.spm.spatial.coreg.write = write; diff --git a/src/workflows/bidsResults.m b/src/workflows/bidsResults.m index f90f0ab3..2208c005 100644 --- a/src/workflows/bidsResults.m +++ b/src/workflows/bidsResults.m @@ -21,7 +21,11 @@ function bidsResults(opt, funcFWHM, conFWHM) % :type conFWHM: scalar % - [~, opt] = setUpWorkflow(opt, 'computing GLM results'); + [BIDS, opt] = setUpWorkflow(opt, 'computing GLM results'); + + if isempty(opt.model.file) + opt = createDefaultModel(BIDS, opt); + end matlabbatch = []; From 42e27a682794a3806d7eb5360495c62751c36ba6 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 1 Apr 2021 15:48:09 +0200 Subject: [PATCH 097/145] make demo download data --- demos/spm_face_rep/face_rep_resolution.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/spm_face_rep/face_rep_resolution.m b/demos/spm_face_rep/face_rep_resolution.m index ce53f18a..6db5e4d9 100644 --- a/demos/spm_face_rep/face_rep_resolution.m +++ b/demos/spm_face_rep/face_rep_resolution.m @@ -10,7 +10,7 @@ FWHM = 6; -downloadData = false; +downloadData = true; run ../../initCppSpm.m; From 391cc143471bd9f525da5ca56e71904982586b00 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 2 Apr 2021 08:21:23 +0200 Subject: [PATCH 098/145] lint --- demos/spm_face_rep/FaceRep_getOption.m | 2 +- demos/spm_face_rep/FaceRep_getOptionResults.m | 40 ++++++++++++++++++ demos/spm_face_rep/face_rep_resolution.m | 41 +------------------ runTests.m | 2 +- src/utils/removeSpmPrefix.m | 2 + 5 files changed, 46 insertions(+), 41 deletions(-) create mode 100644 demos/spm_face_rep/FaceRep_getOptionResults.m diff --git a/demos/spm_face_rep/FaceRep_getOption.m b/demos/spm_face_rep/FaceRep_getOption.m index 2539378a..2db72558 100644 --- a/demos/spm_face_rep/FaceRep_getOption.m +++ b/demos/spm_face_rep/FaceRep_getOption.m @@ -1,4 +1,4 @@ -% (C) Copyright 2019 Remi Gau +% (C) Copyright 2020 Remi Gau function opt = FaceRep_getOption() % returns a structure that contains the options chosen by the user to run diff --git a/demos/spm_face_rep/FaceRep_getOptionResults.m b/demos/spm_face_rep/FaceRep_getOptionResults.m new file mode 100644 index 00000000..b367b1cd --- /dev/null +++ b/demos/spm_face_rep/FaceRep_getOptionResults.m @@ -0,0 +1,40 @@ +% (C) Copyright 2021 Remi Gau + +function opt = FaceRep_getOptionResults() + + opt = FaceRep_getOption(); + + opt.model.file = fullfile( ... + fileparts(mfilename('fullpath')), ... + 'models', ... + 'model-faceRepetition_smdl.json'); + + opt.glmQA.do = false; + + % Specify the result to compute + opt.result.Steps(1) = returnDefaultResultsStructure(); + + opt.result.Steps(1).Level = 'subject'; + + opt.result.Steps(1).Contrasts(1).Name = 'faces_gt_baseline'; + + opt.result.Steps(1).Contrasts(1).MC = 'FWE'; + opt.result.Steps(1).Contrasts(1).p = 0.05; + opt.result.Steps(1).Contrasts(1).k = 5; + + % Specify how you want your output (all the following are on false by default) + opt.result.Steps(1).Output.png = true(); + + opt.result.Steps(1).Output.csv = true(); + + opt.result.Steps(1).Output.thresh_spm = true(); + + opt.result.Steps(1).Output.binary = true(); + + % MONTAGE FIGURE OPTIONS + opt.result.Steps(1).Output.montage.do = true(); + opt.result.Steps(1).Output.montage.slices = -26:3:6; % in mm + % axial is default 'sagittal', 'coronal' + opt.result.Steps(1).Output.montage.orientation = 'axial'; + +end diff --git a/demos/spm_face_rep/face_rep_resolution.m b/demos/spm_face_rep/face_rep_resolution.m index 6db5e4d9..6db1b455 100644 --- a/demos/spm_face_rep/face_rep_resolution.m +++ b/demos/spm_face_rep/face_rep_resolution.m @@ -15,8 +15,8 @@ run ../../initCppSpm.m; %% Set options -opt = FaceRep_getOption(); -opt = FaceRep_getOptionResults(opt); + +opt = FaceRep_getOptionResults(); %% Removes previous analysis, gets data and converts it to BIDS if downloadData @@ -61,40 +61,3 @@ bidsResults(opt, FWHM); end - -function opt = FaceRep_getOptionResults(opt) - - opt.model.file = fullfile( ... - fileparts(mfilename('fullpath')), ... - 'models', ... - 'model-faceRepetition_smdl.json'); - - opt.glmQA.do = false; - - % Specify the result to compute - opt.result.Steps(1) = returnDefaultResultsStructure(); - - opt.result.Steps(1).Level = 'subject'; - - opt.result.Steps(1).Contrasts(1).Name = 'faces_gt_baseline'; - - opt.result.Steps(1).Contrasts(1).MC = 'FWE'; - opt.result.Steps(1).Contrasts(1).p = 0.05; - opt.result.Steps(1).Contrasts(1).k = 5; - - % Specify how you want your output (all the following are on false by default) - opt.result.Steps(1).Output.png = true(); - - opt.result.Steps(1).Output.csv = true(); - - opt.result.Steps(1).Output.thresh_spm = true(); - - opt.result.Steps(1).Output.binary = true(); - - % MONTAGE FIGURE OPTIONS - opt.result.Steps(1).Output.montage.do = true(); - opt.result.Steps(1).Output.montage.slices = -26:3:6; % in mm - % axial is default 'sagittal', 'coronal' - opt.result.Steps(1).Output.montage.orientation = 'axial'; - -end diff --git a/runTests.m b/runTests.m index ac531bf4..f1e02a48 100644 --- a/runTests.m +++ b/runTests.m @@ -1,4 +1,4 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developpers +% (C) Copyright 2019 CPP BIDS SPM-pipeline developers warning('OFF'); diff --git a/src/utils/removeSpmPrefix.m b/src/utils/removeSpmPrefix.m index 9f6a654b..0ea6e3c4 100644 --- a/src/utils/removeSpmPrefix.m +++ b/src/utils/removeSpmPrefix.m @@ -1,3 +1,5 @@ +% (C) Copyright 2019 CPP BIDS SPM-pipeline developers + function image = removeSpmPrefix(image, prefix) basename = spm_file(image, 'basename'); From e637962dad5c570d142298d95992dfc415d931dd Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 2 Apr 2021 09:35:18 +0200 Subject: [PATCH 099/145] make results output name more bids like --- src/batches/setBatchResults.m | 53 ++++++++++++++--------- src/batches/setBatchSubjectLevelResults.m | 12 +++++ src/workflows/bidsFFX.m | 28 ++++++------ 3 files changed, 58 insertions(+), 35 deletions(-) diff --git a/src/batches/setBatchResults.m b/src/batches/setBatchResults.m index fcb6e9f7..1c78a5d6 100644 --- a/src/batches/setBatchResults.m +++ b/src/batches/setBatchResults.m @@ -26,58 +26,69 @@ result = setFields(result, fieldsToSet); result.Contrasts = replaceEmptyFields(result.Contrasts, fieldsToSet.Contrasts); - matlabbatch{end + 1}.spm.stats.results.spmmat = {fullfile(result.dir, 'SPM.mat')}; + stats.results.spmmat = {fullfile(result.dir, 'SPM.mat')}; - matlabbatch{end}.spm.stats.results.conspec.titlestr = returnName(result); + stats.results.conspec.titlestr = returnName(result); - matlabbatch{end}.spm.stats.results.conspec.contrasts = result.contrastNb; - matlabbatch{end}.spm.stats.results.conspec.threshdesc = result.Contrasts.MC; - matlabbatch{end}.spm.stats.results.conspec.thresh = result.Contrasts.p; - matlabbatch{end}.spm.stats.results.conspec.extent = result.Contrasts.k; - matlabbatch{end}.spm.stats.results.conspec.conjunction = 1; - matlabbatch{end}.spm.stats.results.conspec.mask.none = ~result.Contrasts.useMask; + stats.results.conspec.contrasts = result.contrastNb; + stats.results.conspec.threshdesc = result.Contrasts.MC; + stats.results.conspec.thresh = result.Contrasts.p; + stats.results.conspec.extent = result.Contrasts.k; + stats.results.conspec.conjunction = 1; + stats.results.conspec.mask.none = ~result.Contrasts.useMask; - matlabbatch{end}.spm.stats.results.units = 1; + stats.results.units = 1; - matlabbatch{end}.spm.stats.results.export = []; + matlabbatch{end + 1}.spm.stats = stats; + + %% set up how to export the results + export = []; if result.Output.png - matlabbatch{end}.spm.stats.results.export{end + 1}.png = true; + export{end + 1}.png = true; end if result.Output.csv - matlabbatch{end}.spm.stats.results.export{end + 1}.csv = true; + export{end + 1}.csv = true; end if result.Output.thresh_spm - matlabbatch{end}.spm.stats.results.export{end + 1}.tspm.basename = returnName(result); + result.outputNameStructure.ext = ''; + export{end + 1}.tspm.basename = createFilename(result.outputNameStructure); end if result.Output.binary - matlabbatch{end}.spm.stats.results.export{end + 1}.binary.basename = [returnName(result), ... - '_mask']; + result.outputNameStructure.ext = ''; + result.outputNameStructure.type = 'mask'; + export{end + 1}.binary.basename = createFilename(result.outputNameStructure); end if result.Output.NIDM_results - matlabbatch{end}.spm.stats.results.export{end + 1}.nidm.modality = 'FMRI'; + nidm.modality = 'FMRI'; - matlabbatch{end}.spm.stats.results.export{end}.nidm.refspace = 'ixi'; + nidm.refspace = 'ixi'; if strcmp(result.space, 'individual') - matlabbatch{end}.spm.stats.results.export{end}.nidm.refspace = 'subject'; + nidm.refspace = 'subject'; end - matlabbatch{end}.spm.stats.results.export{end}.nidm.group.nsubj = result.nbSubj; + nidm.group.nsubj = result.nbSubj; + + nidm.group.label = result.label; - matlabbatch{end}.spm.stats.results.export{end}.nidm.group.label = result.label; + export{end + 1}.nidm = nidm; end + matlabbatch{end}.spm.stats.results.export = export; + if result.Output.montage.do matlabbatch{end}.spm.stats.results.export{end + 1}.montage = setMontage(result); % Not sure why the name of the figure does not come out right - matlabbatch{end + 1}.spm.util.print.fname = ['Montage_' returnName(result)]; + result.outputNameStructure.ext = ''; + result.outputNameStructure.type = 'montage'; + matlabbatch{end + 1}.spm.util.print.fname = createFilename(result.outputNameStructure); matlabbatch{end}.spm.util.print.fig.figname = 'SliceOverlay'; matlabbatch{end}.spm.util.print.opts = 'png'; diff --git a/src/batches/setBatchSubjectLevelResults.m b/src/batches/setBatchSubjectLevelResults.m index 1f8aa354..92386854 100644 --- a/src/batches/setBatchSubjectLevelResults.m +++ b/src/batches/setBatchSubjectLevelResults.m @@ -39,6 +39,18 @@ result.contrastNb = getContrastNb(result); + result.outputNameStructure = struct( ... + 'type', 'spmT', ... + 'ext', '.nii', ... + 'sub', subLabel, ... + 'task', opt.taskName, ... + 'space', opt.space, ... + 'desc', result.Contrasts.Name, ... + 'label', 'XXXX', ... + 'p', num2str(result.Contrasts.p), ... + 'k', num2str(result.Contrasts.k), ... + 'MC', result.Contrasts.MC); + matlabbatch = setBatchResults(matlabbatch, result); end diff --git a/src/workflows/bidsFFX.m b/src/workflows/bidsFFX.m index f9bc32b1..a56f5f95 100644 --- a/src/workflows/bidsFFX.m +++ b/src/workflows/bidsFFX.m @@ -45,25 +45,25 @@ function bidsFFX(action, opt, funcFWHM) switch action case 'specifyAndEstimate' - - matlabbatch = setBatchSubjectLevelGLMSpec(matlabbatch, BIDS, opt, subLabel, funcFWHM); - - p = struct(... - 'type', 'designmatrix', ... - 'ext', '.png', ... - 'sub', subLabel, ... - 'task', opt.taskName, ... - 'space', opt.space, ... - 'desc', 'before estimation'); - + + matlabbatch = setBatchSubjectLevelGLMSpec(matlabbatch, BIDS, opt, subLabel, funcFWHM); + + p = struct( ... + 'type', 'designmatrix', ... + 'ext', '.png', ... + 'sub', subLabel, ... + 'task', opt.taskName, ... + 'space', opt.space, ... + 'desc', 'before estimation'); + matlabbatch = setBatchPrintFigure(matlabbatch, fullfile(getFFXdir(subLabel, ... funcFWHM, ... opt), ... createFilename(p))); - + matlabbatch = setBatchEstimateModel(matlabbatch, opt); - - p.desc = 'after estimation'; + + p.desc = 'after estimation'; matlabbatch = setBatchPrintFigure(matlabbatch, fullfile(getFFXdir(subLabel, ... funcFWHM, ... opt), ... From c29e9fa4b3bbc0e044991ab67f6e75ed6e766659 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 2 Apr 2021 10:27:44 +0200 Subject: [PATCH 100/145] rename output of results for make the filenames fully bids --- src/workflows/bidsResults.m | 57 +++++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/src/workflows/bidsResults.m b/src/workflows/bidsResults.m index f90f0ab3..9df3a186 100644 --- a/src/workflows/bidsResults.m +++ b/src/workflows/bidsResults.m @@ -23,8 +23,6 @@ function bidsResults(opt, funcFWHM, conFWHM) [~, opt] = setUpWorkflow(opt, 'computing GLM results'); - matlabbatch = []; - % TOD0 % if it does not exist create the default "result" field from the BIDS model file @@ -39,6 +37,7 @@ function bidsResults(opt, funcFWHM, conFWHM) case 'run' warning('run level not implemented yet'); + matlabbatch = []; % saveMatlabBatch(matlabbatch, 'computeFfxResults', opt, subID); case 'subject' @@ -46,8 +45,12 @@ function bidsResults(opt, funcFWHM, conFWHM) % For each subject for iSub = 1:numel(opt.subjects) + matlabbatch = []; + subLabel = opt.subjects{iSub}; + results.dir = getFFXdir(subLabel, funcFWHM, opt); + for iCon = 1:length(opt.result.Steps(iStep).Contrasts) matlabbatch = ... @@ -61,14 +64,20 @@ function bidsResults(opt, funcFWHM, conFWHM) end - end + batchName = sprintf('compute_sub-%s_results', subLabel); - batchName = sprintf('compute_sub-%s_results', subLabel); + saveAndRunWorkflow(matlabbatch, batchName, opt, subLabel); - saveAndRunWorkflow(matlabbatch, batchName, opt, subLabel); + renameOutputResults(results); + + renamePng(results); + + end case 'dataset' + matlabbatch = []; + results.dir = getRFXdir(opt, funcFWHM, conFWHM); results.contrastNb = 1; results.label = 'group level'; @@ -101,3 +110,41 @@ function bidsResults(opt, funcFWHM, conFWHM) % TODO end + +function renameOutputResults(results) + % we create new name for the nifti oupput by removing the + % spmT_XXXX prefix and using the XXXX as label- for the file + + outputFiles = spm_select('FPList', results.dir, '^spmT_[0-9].*_sub-.*.nii$'); + + for iFile = 1:size(outputFiles, 1) + + source = deblank(outputFiles(iFile, :)); + + basename = spm_file(source, 'basename'); + split = strfind(basename, '_sub'); + p = bids.internal.parse_filename(basename(split + 1:end)); + p.label = basename(split - 4:split - 1); + newName = createFilename(p); + + target = spm_file(source, 'basename', newName); + + movefile(source, target); + end + +end + +function renamePng(results) + % + % removes the _XXX suffix before the PNG extension. + + pngFiles = spm_select('FPList', results.dir, '^sub-.*[0-9].png$'); + + for iFile = 1:size(pngFiles, 1) + source = deblank(pngFiles(iFile, :)); + basename = spm_file(source, 'basename'); + target = spm_file(source, 'basename', basename(1:end - 4)); + movefile(source, target); + end + +end From 0d81e4862fcd6a2ffe91aa17c2774bd46a5b6b40 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 2 Apr 2021 11:41:06 +0200 Subject: [PATCH 101/145] refactor and update tests --- src/batches/setBatchResults.m | 5 +++ src/batches/setBatchSubjectLevelResults.m | 10 ++--- src/workflows/bidsResults.m | 2 +- tests/test_setBatchResults.m | 48 +++++++++++++++++------ 4 files changed, 46 insertions(+), 19 deletions(-) diff --git a/src/batches/setBatchResults.m b/src/batches/setBatchResults.m index 1c78a5d6..84b73eab 100644 --- a/src/batches/setBatchResults.m +++ b/src/batches/setBatchResults.m @@ -21,6 +21,11 @@ % :returns: - :matlabbatch: (structure) % % + result.outputNameStructure.sub = result.label; + result.outputNameStructure.desc = result.Contrasts.Name; + result.outputNameStructure.p = num2str(result.Contrasts.p); + result.outputNameStructure.k = num2str(result.Contrasts.k); + result.outputNameStructure.MC = result.Contrasts.MC; fieldsToSet = returnDefaultResultsStructure(); result = setFields(result, fieldsToSet); diff --git a/src/batches/setBatchSubjectLevelResults.m b/src/batches/setBatchSubjectLevelResults.m index 92386854..ff2638d3 100644 --- a/src/batches/setBatchSubjectLevelResults.m +++ b/src/batches/setBatchSubjectLevelResults.m @@ -42,14 +42,14 @@ result.outputNameStructure = struct( ... 'type', 'spmT', ... 'ext', '.nii', ... - 'sub', subLabel, ... + 'sub', '', ... 'task', opt.taskName, ... 'space', opt.space, ... - 'desc', result.Contrasts.Name, ... + 'desc', '', ... 'label', 'XXXX', ... - 'p', num2str(result.Contrasts.p), ... - 'k', num2str(result.Contrasts.k), ... - 'MC', result.Contrasts.MC); + 'p', '', ... + 'k', '', ... + 'MC', ''); matlabbatch = setBatchResults(matlabbatch, result); diff --git a/src/workflows/bidsResults.m b/src/workflows/bidsResults.m index 9df3a186..7420aca4 100644 --- a/src/workflows/bidsResults.m +++ b/src/workflows/bidsResults.m @@ -80,7 +80,7 @@ function bidsResults(opt, funcFWHM, conFWHM) results.dir = getRFXdir(opt, funcFWHM, conFWHM); results.contrastNb = 1; - results.label = 'group level'; + results.label = 'group'; load(fullfile(results.dir, 'SPM.mat')); results.nbSubj = SPM.nscan; diff --git a/tests/test_setBatchResults.m b/tests/test_setBatchResults.m index 1a39cfa5..4efd602e 100644 --- a/tests/test_setBatchResults.m +++ b/tests/test_setBatchResults.m @@ -18,6 +18,11 @@ function test_setBatchResultsBasic() result.nbSubj = 1; result.contrastNb = 1; + result.Contrasts.Name = ''; + result.Contrasts.MC = 'FWE'; + result.Contrasts.p = 0.05; + result.Contrasts.k = 0; + matlabbatch = []; matlabbatch = setBatchResults(matlabbatch, result); @@ -32,6 +37,8 @@ function test_setBatchResultsExport() iStep = 1; iCon = 1; + opt.taskName = 'test'; + opt.result.Steps.Contrasts.Name = 'test'; opt.result.Steps.Contrasts.MC = 'FDR'; opt.result.Steps.Contrasts.p = 0.05; @@ -51,6 +58,18 @@ function test_setBatchResultsExport() result.contrastNb = 1; %% + result.outputNameStructure = struct( ... + 'type', 'spmT', ... + 'ext', '.nii', ... + 'sub', '', ... + 'task', opt.taskName, ... + 'space', opt.space, ... + 'desc', '', ... + 'label', 'XXXX', ... + 'p', '', ... + 'k', '', ... + 'MC', ''); + result.Contrasts = opt.result.Steps(iStep).Contrasts; result.Output = opt.result.Steps(iStep).Output; result.space = opt.space; @@ -66,8 +85,10 @@ function test_setBatchResultsExport() expectedBatch{end}.spm.stats.results.export{1}.png = true; expectedBatch{end}.spm.stats.results.export{2}.csv = true; - expectedBatch{end}.spm.stats.results.export{3}.tspm.basename = returnName(result); - expectedBatch{end}.spm.stats.results.export{4}.binary.basename = [returnName(result) '_mask']; + expectedBatch{end}.spm.stats.results.export{3}.tspm.basename = ... + 'sub-01_task-test_space-individual_desc-test_label-XXXX_p-005_k-0_MC-FDR_spmT'; + expectedBatch{end}.spm.stats.results.export{4}.binary.basename = ... + 'sub-01_task-test_space-individual_desc-test_label-XXXX_p-005_k-0_MC-FDR_mask'; expectedBatch{end}.spm.stats.results.export{end + 1}.nidm.modality = 'FMRI'; expectedBatch{end}.spm.stats.results.export{end}.nidm.refspace = 'ixi'; @@ -130,23 +151,24 @@ function test_setBatchResultsMontage() function expectedBatch = returnBasicExpectedResultsBatch() result.Contrasts.Name = ''; + result.Contrasts.MC = 'FWE'; result.Contrasts.p = 0.05; result.Contrasts.k = 0; - result.Contrasts.MC = 'FWE'; - expectedBatch = {}; - expectedBatch{end + 1}.spm.stats.results.spmmat = {fullfile(pwd, 'SPM.mat')}; + stats.results.spmmat = {fullfile(pwd, 'SPM.mat')}; - expectedBatch{end}.spm.stats.results.conspec.titlestr = returnName(result); - expectedBatch{end}.spm.stats.results.conspec.contrasts = 1; - expectedBatch{end}.spm.stats.results.conspec.threshdesc = 'FWE'; - expectedBatch{end}.spm.stats.results.conspec.thresh = 0.05; - expectedBatch{end}.spm.stats.results.conspec.extent = 0; - expectedBatch{end}.spm.stats.results.conspec.conjunction = 1; - expectedBatch{end}.spm.stats.results.conspec.mask.none = true(); + stats.results.conspec.titlestr = returnName(result); + stats.results.conspec.contrasts = 1; + stats.results.conspec.threshdesc = 'FWE'; + stats.results.conspec.thresh = 0.05; + stats.results.conspec.extent = 0; + stats.results.conspec.conjunction = 1; + stats.results.conspec.mask.none = true(); - expectedBatch{end}.spm.stats.results.units = 1; + stats.results.units = 1; + expectedBatch = {}; + expectedBatch{end + 1}.spm.stats = stats; expectedBatch{end}.spm.stats.results.export = []; end From 398cc94a84b4cc6fc8e8c747c7cea2c1ce22c005 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 2 Apr 2021 12:51:22 +0200 Subject: [PATCH 102/145] update submodule --- lib/CPP_ROI | 2 +- src/utils/converToValidCamelCase.m | 30 ------------------------------ 2 files changed, 1 insertion(+), 31 deletions(-) delete mode 100644 src/utils/converToValidCamelCase.m diff --git a/lib/CPP_ROI b/lib/CPP_ROI index f1f7d5f0..fa2e5c23 160000 --- a/lib/CPP_ROI +++ b/lib/CPP_ROI @@ -1 +1 @@ -Subproject commit f1f7d5f0867d34b2ea26b60640fc3848f48d8b48 +Subproject commit fa2e5c23bcfd445e693e8bf0cd6e8f4c455f718b diff --git a/src/utils/converToValidCamelCase.m b/src/utils/converToValidCamelCase.m deleted file mode 100644 index 99642b99..00000000 --- a/src/utils/converToValidCamelCase.m +++ /dev/null @@ -1,30 +0,0 @@ -% (C) Copyright 2021 CPP BIDS SPM-pipeline developers - -function str = converToValidCamelCase(str) - % - % Removes non alphanumeric characters and uppercase first letter of all - % words but the first - % - % USAGE:: - % - % str = converToValidCamelCase(str) - % - % :param str: - % :type str: string - % - % :returns: - % :str: (string) returns the input with an upper case for first letter - % for all words but the first one (``camelCase``) and - % removes invalid characters (like spaces). - % - % - - % camel case: upper case for first letter for all words but the first one - spaceIdx = regexp(str, '[a-zA-Z0-9]*', 'start'); - str(spaceIdx(2:end)) = upper(str(spaceIdx(2:end))); - - % remove invalid characters - [unvalidCharacters] = regexp(str, '[^a-zA-Z0-9]'); - str(unvalidCharacters) = []; - -end From d53b3e2877f6ab3a9e5e8cf5067166a61321a961 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 2 Apr 2021 13:06:59 +0200 Subject: [PATCH 103/145] update tests --- tests/test_createAndReturnOnsetFile.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_createAndReturnOnsetFile.m b/tests/test_createAndReturnOnsetFile.m index a31aa574..d23ec58f 100644 --- a/tests/test_createAndReturnOnsetFile.m +++ b/tests/test_createAndReturnOnsetFile.m @@ -30,7 +30,7 @@ function test_createAndReturnOnsetFileBasic() expectedFileName = fullfile(fileparts(mfilename('fullpath')), ... 'dummyData', 'derivatives', 'cpp_spm', 'sub-01', 'stats', ... 'task-vislocalizer_space-MNI_FWHM-6', ... - 'onsets_sub-01_ses-01_task-vislocalizer_events.mat'); + 'sub-01_ses-01_task-vislocalizer_space-MNI_onsets.mat'); assertEqual(exist(onsetFileName, 'file'), 2); assertEqual(exist(expectedFileName, 'file'), 2); From a56c62e71947e56e3ac4149e02064a649eadcb3f Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 2 Apr 2021 13:36:07 +0200 Subject: [PATCH 104/145] update submodule --- lib/CPP_ROI | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/CPP_ROI b/lib/CPP_ROI index fa2e5c23..39239801 160000 --- a/lib/CPP_ROI +++ b/lib/CPP_ROI @@ -1 +1 @@ -Subproject commit fa2e5c23bcfd445e693e8bf0cd6e8f4c455f718b +Subproject commit 39239801be85dc8b191c3b4f5615debfa6e9014e From b2e012593ba42e6b4c9b087af62f96de39920b66 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Fri, 2 Apr 2021 14:25:36 +0200 Subject: [PATCH 105/145] add demo to create ROI and extract data from GLM --- .gitignore | 1 + demos/MoAE/MoAE_create_roi_extract_data.m | 44 +++++++++++++++++++++++ lib/CPP_ROI | 2 +- 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 demos/MoAE/MoAE_create_roi_extract_data.m diff --git a/.gitignore b/.gitignore index fadce1c0..547a6cb5 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ onsets*_events.mat # files in the demo folder related to running the demo analysis demos/*/outputs/ demos/*/inputs/ +demos/*/*.nii demos/*/cfg/*.json # test folder and dummy data diff --git a/demos/MoAE/MoAE_create_roi_extract_data.m b/demos/MoAE/MoAE_create_roi_extract_data.m new file mode 100644 index 00000000..41306be7 --- /dev/null +++ b/demos/MoAE/MoAE_create_roi_extract_data.m @@ -0,0 +1,44 @@ +%% Create ROI and extract data from it +% +% FYI: this is "double dipping" as we use the same data to create the ROI +% we are going to extract the value from. +% + +run MoAEpilot_run.m; + +subLabel = '01'; + +saveROI = true; + +sphere.location = [57 -22 11]; +sphere.radius = 3; +sphere.maxNbVoxels = 200; + +opt = setDerivativesDir(opt); +ffxDir = getFFXdir(subLabel, FWHM, opt); + +maskImage = spm_select('FPList', ffxDir, '^.*_mask.nii$'); + +% we get the con image to extract data +% we can do this by using the "label-XXXX" from the mask +p = bids.internal.parse_filename(spm_file(maskImage, 'filename')); + +conImage = spm_select('FPList', ffxDir, ['^con_' p.label '.nii$']); + +specification = struct( ... + 'mask1', maskImage, ... + 'mask2', sphere); + +[~, roiFile] = createRoi('expand', specification, conImage, pwd, saveROI); + +% rename mask image +newname.desc = 'left auditory cortex'; +newname.task = ''; +newname.label = ''; +newname.p = ''; +newname.k = ''; +newname.MC = ''; +roiFile = renameFile(roiFile, newname); + +data = spm_summarise(conImage, roiFile); + diff --git a/lib/CPP_ROI b/lib/CPP_ROI index 39239801..ebcf024b 160000 --- a/lib/CPP_ROI +++ b/lib/CPP_ROI @@ -1 +1 @@ -Subproject commit 39239801be85dc8b191c3b4f5615debfa6e9014e +Subproject commit ebcf024baeeacc90280f8e065eb8c423a3e534be From a04294be16aa5cac95bd1b8695a05907d6ba5e70 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 11 Mar 2021 21:05:08 +0100 Subject: [PATCH 106/145] add slice display and depenencies --- .gitmodules | 3 + lib/panel-2.14/demo/demopanel1.m | 130 + lib/panel-2.14/demo/demopanel2.m | 64 + lib/panel-2.14/demo/demopanel3.m | 106 + lib/panel-2.14/demo/demopanel4.m | 59 + lib/panel-2.14/demo/demopanel5.m | 74 + lib/panel-2.14/demo/demopanel6.m | 82 + lib/panel-2.14/demo/demopanel7.m | 35 + lib/panel-2.14/demo/demopanel8.m | 40 + lib/panel-2.14/demo/demopanel9.m | 201 + lib/panel-2.14/demo/demopanelA.m | 128 + lib/panel-2.14/demo/demopanelB.m | 42 + lib/panel-2.14/demo/demopanelC.m | 34 + lib/panel-2.14/demo/demopanelD.m | 55 + lib/panel-2.14/demo/demopanelE.m | 73 + lib/panel-2.14/demo/demopanelF.m | 49 + lib/panel-2.14/demo/demopanelG.m | 50 + lib/panel-2.14/demo/demopanelH.m | 38 + lib/panel-2.14/demo/demopanelI.m | 98 + lib/panel-2.14/demo/demopanelJ.m | 39 + lib/panel-2.14/demo/demopanelK.m | 103 + lib/panel-2.14/demo/demopanel_callback.m | 25 + lib/panel-2.14/demo/demopanel_minihist.m | 80 + lib/panel-2.14/docs/demopanelI.png | Bin 0 -> 101545 bytes lib/panel-2.14/docs/export.png | Bin 0 -> 182653 bytes lib/panel-2.14/docs/export_thumb.png | Bin 0 -> 30941 bytes lib/panel-2.14/docs/index.png | Bin 0 -> 268564 bytes lib/panel-2.14/docs/index_screenshot.png | Bin 0 -> 115026 bytes lib/panel-2.14/docs/index_thumb.png | Bin 0 -> 65629 bytes lib/panel-2.14/docs/layout.png | Bin 0 -> 248076 bytes lib/panel-2.14/docs/layout_thumb.png | Bin 0 -> 56561 bytes lib/panel-2.14/docs/panel.css | 63 + lib/panel-2.14/license.txt | 25 + lib/panel-2.14/panel.m | 5201 ++++++++++++++++++++++ lib/slice_display | 1 + 35 files changed, 6898 insertions(+) create mode 100644 lib/panel-2.14/demo/demopanel1.m create mode 100644 lib/panel-2.14/demo/demopanel2.m create mode 100644 lib/panel-2.14/demo/demopanel3.m create mode 100644 lib/panel-2.14/demo/demopanel4.m create mode 100644 lib/panel-2.14/demo/demopanel5.m create mode 100644 lib/panel-2.14/demo/demopanel6.m create mode 100644 lib/panel-2.14/demo/demopanel7.m create mode 100644 lib/panel-2.14/demo/demopanel8.m create mode 100644 lib/panel-2.14/demo/demopanel9.m create mode 100644 lib/panel-2.14/demo/demopanelA.m create mode 100644 lib/panel-2.14/demo/demopanelB.m create mode 100644 lib/panel-2.14/demo/demopanelC.m create mode 100644 lib/panel-2.14/demo/demopanelD.m create mode 100644 lib/panel-2.14/demo/demopanelE.m create mode 100644 lib/panel-2.14/demo/demopanelF.m create mode 100644 lib/panel-2.14/demo/demopanelG.m create mode 100644 lib/panel-2.14/demo/demopanelH.m create mode 100644 lib/panel-2.14/demo/demopanelI.m create mode 100644 lib/panel-2.14/demo/demopanelJ.m create mode 100644 lib/panel-2.14/demo/demopanelK.m create mode 100644 lib/panel-2.14/demo/demopanel_callback.m create mode 100644 lib/panel-2.14/demo/demopanel_minihist.m create mode 100644 lib/panel-2.14/docs/demopanelI.png create mode 100644 lib/panel-2.14/docs/export.png create mode 100644 lib/panel-2.14/docs/export_thumb.png create mode 100644 lib/panel-2.14/docs/index.png create mode 100644 lib/panel-2.14/docs/index_screenshot.png create mode 100644 lib/panel-2.14/docs/index_thumb.png create mode 100644 lib/panel-2.14/docs/layout.png create mode 100644 lib/panel-2.14/docs/layout_thumb.png create mode 100644 lib/panel-2.14/docs/panel.css create mode 100644 lib/panel-2.14/license.txt create mode 100644 lib/panel-2.14/panel.m create mode 160000 lib/slice_display diff --git a/.gitmodules b/.gitmodules index 483bea38..c749fe1e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "lib/CPP_ROI"] path = lib/CPP_ROI url = https://github.com/cpp-lln-lab/CPP_ROI.git +[submodule "lib/slice_display"] + path = lib/slice_display + url = https://github.com/bramzandbelt/slice_display.git diff --git a/lib/panel-2.14/demo/demopanel1.m b/lib/panel-2.14/demo/demopanel1.m new file mode 100644 index 00000000..c920b3ef --- /dev/null +++ b/lib/panel-2.14/demo/demopanel1.m @@ -0,0 +1,130 @@ + +% What can Panel do? +% +% This demo just shows off what Panel can do. It is not +% intended as part of the tutorial - this begins in +% demopanel2. +% +% (a) It's easy to create a complex layout +% (b) You can populate it as you would a subplot layout +% +% Now, move on to demopanel2 to learn how to use panel. + + + +%% (a) + +% clf +figure(1) +clf + +% create panel +p = panel(); + +% layout a variety of sub-panels +p.pack('h', {1/3 []}) +p(1).pack({2/3 []}); +p(1,1).pack(3, 2); +p(2).pack(6, 2); + +% set margins +p.de.margin = 2; +p(1,1).marginbottom = 12; +p(2).marginleft = 20; +p.margin = [13 10 2 2]; + +% and some properties +p.fontsize = 8; + + + +%% (b) + +% data set 1 +for m = 1:3 + for n = 1:2 + + % prepare sample data + t = (0:99) / 100; + s1 = sin(t * 2 * pi * m); + s2 = sin(t * 2 * pi * n * 2); + + % select axis - see data set 2 for an alternative way to + % access sub-panels + p(1,1,m,n).select(); + + % plot + plot(t, s1, 'r', 'linewidth', 1); + hold on + plot(t, s2, 'b', 'linewidth', 1); + plot(t, s1+s2, 'k', 'linewidth', 1); + + % finalise axis + axis([0 1 -2.2 2.2]); + set(gca, 'xtick', [], 'ytick', []); + + end +end + +% label axis group +p(1,1).xlabel('time (unitless)'); +p(1,1).ylabel('example data series'); + +% data set 2 +source = 'XYZXYZ'; + +% an alternative way to access sub-panels is to first get a +% reference to the parent... +q = p(2); + +% loop +for m = 1:6 + for n = 1:2 + + % select axis - these two lines do the same thing (see + % above) +% p(2, m, n).select(); + q(m, n).select(); + + % prepare sample data + data = randn(100, 1) * 0.4; + + % do stats + stats = []; + stats.source = source(m); + stats.binrange = [-1 1]; + stats.xtick = [-0.8:0.4:0.8]; + stats.ytick = [0 20]; + stats.bincens = -0.9:0.2:0.9; + stats.values = data; + stats.freq = hist(data, stats.bincens); + stats.percfreq = stats.freq / length(data) * 100; + stats.percpeak = 30; + + % plot + demopanel_minihist(stats, m == 6, n == 1); + + end +end + +% label axis group +p(2).xlabel('data value (furlongs per fortnight)'); +p(2).ylabel('normalised frequency (%)'); + +% data set 3 +p(1, 2).select(); + +% prepare sample data +r1 = rand(100, 1); +r2 = randn(100, 1); + +% plot +plot(r1, r1+0.2*r2, 'k.') +hold on +plot([0 1], [0 1], 'r-') + +% finalise axis +xlabel('our predictions'); +ylabel('actual measurements') + + diff --git a/lib/panel-2.14/demo/demopanel2.m b/lib/panel-2.14/demo/demopanel2.m new file mode 100644 index 00000000..dce29fd4 --- /dev/null +++ b/lib/panel-2.14/demo/demopanel2.m @@ -0,0 +1,64 @@ + +% Basic use. Panel is just like subplot. +% +% (a) Create a grid of panels. +% (b) Plot into each sub-panel. + + + +%% (a) + +% create a NxN grid in gcf (this will create a figure, if +% none is open). +% +% you can pass the figure handle to the constructor if you +% need to attach the panel to a particular figure, as: +% +% p = panel(h_figure) +% +% NB: you can use this code to compare using panel() with +% using subplot(). you should find they do much the same +% thing in this case, but with a slightly different layout. + +N = 2; +use_panel = 1; +clf + +% PREPARE +if use_panel + p = panel(); + p.pack(N, N); +end + + + +%% (b) + +% plot into each panel in turn + +for m = 1:N + for n = 1:N + + % select one of the NxN grid of sub-panels + if use_panel + p(m, n).select(); + else + subplot(N, N, m + (n-1) * N); + end + + % plot some data + plot(randn(100,1)); + + % you can use all the usual calls + xlabel('sample number'); + ylabel('data'); + + % and so on - generally, you can treat the axis panel + % like any other axis + axis([0 100 -3 3]); + + end +end + + + diff --git a/lib/panel-2.14/demo/demopanel3.m b/lib/panel-2.14/demo/demopanel3.m new file mode 100644 index 00000000..21d0ed16 --- /dev/null +++ b/lib/panel-2.14/demo/demopanel3.m @@ -0,0 +1,106 @@ + +% You can nest Panels as much as you like. +% +% (a) Create a grid of panels. +% (b) Plot into three of the sub-panels. +% (c) Create another grid in the fourth. +% (d) Plot into each of these. + + + +%% (a) + +% create a panel in gcf. +% +% "p" is called the "root panel", which is the special panel +% whose parent is the figure window (usually), rather than +% another panel. +p = panel(); + +% pack a 2x2 grid of panels into it. +p.pack(2, 2); + + + +%% (b) + +% plot into the first three panels +for m = 1:2 + for n = 1:2 + + % skip the 2,2 panel + if m == 2 && n == 2 + break + end + + % select the panel (create an axis, and make that axis + % current) + p(m, n).select(); + + % plot some stuff + plot(randn(100,1)); + xlabel('sample number'); + ylabel('data'); + axis([0 100 -3 3]); + + end +end + + + +%% (c) + +% pack a further grid into p(2, 2) +% +% all panels start as "uncommitted panels" (even the root +% panel). the first time we "select()" one, we commit it as +% an "axis panel". the first time we "pack()" one, we commit +% it as a "parent panel". once committed, it can't change +% into the other sort. +% +% this call commits p(2,2) as a parent panel - the six +% children it creates all start as uncommitted panels. +p(2, 2).pack(2, 3); + + + +%% (d) + +% plot into the six new sub-sub-panels +for m = 1:2 + for n = 1:3 + + % select the panel - this commits it as an axis panel + p(2, 2, m, n).select(); + + % plot some stuff + plot(randn(100,1)); + xlabel('sample number'); + ylabel('data'); + axis([0 100 -3 3]); + + end +end + +% note this alternative, equivalent, way to reference a +% sub-panel +p_22 = p(2, 2); + +% plot another bit of data into the six sub-sub-panels +for m = 1:2 + for n = 1:3 + + % select the panel + p_22(m, n).select(); + + % plot more stuff + hold on + plot(randn(100,1)*0.3, 'r'); + + end +end + + + + + diff --git a/lib/panel-2.14/demo/demopanel4.m b/lib/panel-2.14/demo/demopanel4.m new file mode 100644 index 00000000..41b83536 --- /dev/null +++ b/lib/panel-2.14/demo/demopanel4.m @@ -0,0 +1,59 @@ + +% Panels can be any size. +% +% (a) Create an asymmetrical grid of panels. +% (b) Create another. +% (c) Use select('all') to load them all with axes +% (d) Get handles to all the axes and modify them. + + + +%% (a) + +% create a 2x2 grid in gcf with different fractionally-sized +% rows and columns. a row or column sized as "[]" will +% stretch to fill the remaining unassigned space. +p = panel(); +p.pack({1/3 []}, {1/3 []}); + + + +%% (b) + +% pack a 2x3 grid into p(2, 2). note that we can pack by +% percentage as well as by fraction - the interpretation is +% just based on the size of the numbers we pass in (1 to +% 100 for percentage, or 0 to 1 for fraction). +p(2, 2).pack({30 70}, {20 20 []}); + + + +%% (c) + +% use select('all') to quickly show the layout you've achieved. +% this commits all uncommitted panels as axis panels, so +% they can't be parents anymore (i.e. they can't have more +% children pack()ed into them). +% +% this is no use at all once you've got organised - look at +% the first three demos, which don't use it - but it may help +% you to see what you're doing as you're starting out. +p.select('all'); + + + +%% (d) + +% whilst we're here, we can get all the axes within a +% particular panel like this. there are three "groups" +% associated with a panel: (fa)mily, (de)scendants, and +% (ch)ildren. see "help panel/descendants", for instance, to +% see who's in them. they're each useful in different +% circumstances. here, we use (de)scendants. +h_axes = p.de.axis; + +% so then we might want to set something on them. +set(h_axes, 'color', [0 0 0]); + +% yeah, real gothic. + diff --git a/lib/panel-2.14/demo/demopanel5.m b/lib/panel-2.14/demo/demopanel5.m new file mode 100644 index 00000000..9074da4a --- /dev/null +++ b/lib/panel-2.14/demo/demopanel5.m @@ -0,0 +1,74 @@ + +% Tools for finding your way around a layout. +% +% (a) Recreate the complex layout from demopanel1 +% (b) Show three tools that help to navigate a layout + + + +%% (a) + +% create panel +p = panel(); + +% layout a variety of sub-panels +p.pack('h', {1/3 []}) +p(1).pack({2/3 []}); +p(1,1).pack(3, 2); +p(2).pack(6, 2); + +% set margins +p.de.margin = 10; +p(1,1).marginbottom = 20; +p(2).marginleft = 20; +p.margin = [13 10 2 2]; + +% set some font properties +p.fontsize = 8; + + + +%% (b) + +% if a layout gets complex, it can be tricky to find your +% way around it. it's quite natural once you get the hang, +% but there are three tools that will help you if you get +% lost. they are display(), identify() and show(). + +% identify() only works on axis panels. we haven't bothered +% plotting any data, this time, so we'll use select('all') +% to commit all remaining uncommitted panels as axis panels. +p.select('all'); + +% display() the panel object at the prompt +% +% notice that most of the panels are called "Object" - this +% is because they are "object panels", which is the general +% name for axis panels (and that's because panels can contain +% other graphics objects as well as axes). +p + +% use identify() +% +% every panel that is an axis panel has its axis wiped and +% replaced with the panel's reference. the one in the bottom +% right, for instance, is labelled "(2,6,2)", which means we +% can access it with p(2,6,2). +p.identify(); + +% use show() +% +% we can demonstrate this by using this tool. the selected +% panel is highlighted in red. show works on parent panels +% as well - try "p(2).show()", for instance. +p(2,6,2).show(); + +% just to prove the point, let's now select one of the +% panels we've identified and plot something into it. +p(2,4,1).select(); +plot(randn(100, 1)) +axis auto + + + + diff --git a/lib/panel-2.14/demo/demopanel6.m b/lib/panel-2.14/demo/demopanel6.m new file mode 100644 index 00000000..18499f54 --- /dev/null +++ b/lib/panel-2.14/demo/demopanel6.m @@ -0,0 +1,82 @@ + +% Packing is very flexible - it doesn't just do grids. +% +% (a) Pack a pair of columns. +% (b) Pack a bit into one of them, and then pack some more. +% (c) Pack into the other using absolute packing mode. +% (d) Call select('all'), to show off the result. + + + +%% (a) + +% create the root panel, and pack two columns. to pack +% columns instead of rows, we just pass "h" (horizontal) to +% pack(). +p = panel(); +p.pack('h', 2); + + + +%% (b) + +% pack some stuff into the left column. +p(1).pack({1/6 1/6 1/6}); + +% oops, we didn't fill the thing. let's finish that off with +% a couple of panels that are streeeeeeeee-tchy... +p(1).pack(); +p(1).pack(); + +% we could have also called p(1).pack(2) to do both at once, +% or one call could even have done all five if we'd passed +% enough arguments in the first place (remember we can pass +% [] to leave a panel stretchy). it would have looked like +% this: +% +% p(1).pack({1/6 1/6 1/6 [] []}); + +% see help panel/pack or doc panel for more information on +% the packing possibilities. + + + +%% (c) + +% in the other column, we'll show how to do absolute mode +% packing. perhaps you're unlikely to need this, but it's +% there if you do. with absolute mode, you can even place +% the child panel outside of its parent's area. just pass a +% 4-element row vector of [left bottom width height] to do +% absolute mode packing. +p(2).pack({[-0.3 -0.01 1 0.4]}); + +% just to show that you can do relative and absolute +% alongside, we'll pack a relative mode panel as well. +p(2).pack(); + +% you can pack more than one absolute mode, of course. this +% one comes out on top of the relative mode panel, because +% it was created later, though you can mess with the +% z-orders in the usual matlab way if you need to. +p(2).pack({[0.2 0.61 0.6 0.4]}); + +% see help panel/pack or doc panel for more information on +% the packing possibilities. + + + +%% (d) + +% use selectAll to quickly show the layout you've achieved. +% this commits all uncommitted panels as axis panels, so +% they can't be parents anymore (i.e. they can't have more +% children pack()ed into them). +p.select('all'); + + + + + + + diff --git a/lib/panel-2.14/demo/demopanel7.m b/lib/panel-2.14/demo/demopanel7.m new file mode 100644 index 00000000..ea883197 --- /dev/null +++ b/lib/panel-2.14/demo/demopanel7.m @@ -0,0 +1,35 @@ + +% Panel gives you figure-wide control over text properties. +% +% (a) Create a grid of panels. +% (b) Change some text properties. + + + +%% (a) + +% create a grid +p = panel(); +p.pack(2, 2); + +% select all +p.select('all'); + + + + + +%% (b) + +% if we set the properties on the root panel, they affect +% all its children and grandchildren. +p.fontname = 'Courier New'; +p.fontsize = 10; +p.fontweight = 'normal'; % this is the default, anyway + +% however, any child can override them, and the changes +% affect just that child (and its descendants). +p(2,2).fontsize = 14; + + + diff --git a/lib/panel-2.14/demo/demopanel8.m b/lib/panel-2.14/demo/demopanel8.m new file mode 100644 index 00000000..266c7d8a --- /dev/null +++ b/lib/panel-2.14/demo/demopanel8.m @@ -0,0 +1,40 @@ + +% You can repack Panels from the command line. +% +% (a) Create a grid of panels, and show something in them. +% (b) Repack some of them, as if at the command line. + + + +%% (a) + +% create a 2x2 grid in gcf. +p = panel(); +p.pack(2, 2); + +% have a look at p - all the child panels are currently +% uncommitted +p + +% commit all the uncommitted panels as axis panels +p.select('all'); + + + +%% (b) + +% during development of a layout, you might find repack() +% useful. + +% repack one of the rows in the root panel +p(1).repack(0.3); + +% repack one of the columns in one of the rows +p(1, 1).repack(0.3); + +% remember, you can always get a summary of the layout by +% looking at the root panel in the command window +p + + + diff --git a/lib/panel-2.14/demo/demopanel9.m b/lib/panel-2.14/demo/demopanel9.m new file mode 100644 index 00000000..52100d77 --- /dev/null +++ b/lib/panel-2.14/demo/demopanel9.m @@ -0,0 +1,201 @@ + +% Panel can build complex layouts rapidly (HINTS on MARGINS!). +% +% (a) Build the layout from demopanel1, with annotation +% (b) Add the content, so we can see what we're aiming for +% (c) Show labelling of axis groups +% (d) Add appropriate margins for this layout + + + +%% (a) + +% create panel +p = panel(); + +% let's start with two columns, one third and two thirds +p.pack('h', {1/3 2/3}) + +% then let's pack two rows into the first column, with the +% top row pretty big so we've room for some sub-panels +p(1).pack({2/3 []}); + +% now let's pack in those sub-panels +p(1,1).pack(3, 2); + +% finally, let's pack a grid of sub-panels into the right +% hand side too +p(2).pack(6, 2); + + + +%% (b) + +% now, let's populate those panels with axes full of data... + +% data set 1 +for m = 1:3 + for n = 1:2 + + % prepare sample data + t = (0:99) / 100; + s1 = sin(t * 2 * pi * m); + s2 = sin(t * 2 * pi * n * 2); + + % select axis + p(1,1,m,n).select(); + + % NB: an alternative way of accessing + % q = p(1, 1); + % q(m, n).select(); + + % plot + plot(t, s1, 'r', 'linewidth', 1); + hold on + plot(t, s2, 'b', 'linewidth', 1); + plot(t, s1+s2, 'k', 'linewidth', 1); + + % finalise axis + axis([0 1 -2.2 2.2]); + set(gca, 'xtick', [], 'ytick', []); + + end +end + +% data set 2 +source = 'XYZXYZ'; + +for m = 1:6 + for n = 1:2 + + % select axis + p(2,m,n).select(); + + % prepare sample data + data = randn(100, 1) * 0.4; + + % do stats + stats = []; + stats.source = source(m); + stats.binrange = [-1 1]; + stats.xtick = [-0.8:0.4:0.8]; + stats.ytick = [0 20 40]; + stats.bincens = -0.9:0.2:0.9; + stats.values = data; + stats.freq = hist(data, stats.bincens); + stats.percfreq = stats.freq / length(data) * 100; + stats.percpeak = 30; + + % plot + demopanel_minihist(stats, m == 6, n == 1); + + end +end + +% data set 3 +p(1, 2).select(); + +% prepare sample data +r1 = rand(100, 1); +r2 = randn(100, 1); + +% plot +plot(r1, r1+0.2*r2, 'k.') +hold on +plot([0 1], [0 1], 'r-') + +% finalise axis +xlabel('our predictions'); +ylabel('actual measurements') + + + +%% (c) + +% we can label parent panels (or, "axis groups") just like +% labelling axis panels, except we have to use the method +% from panel, rather than the matlab call xlabel(). + +% label axis group +p(1,1).xlabel('time (unitless)'); +p(1,1).ylabel('example data series'); + +% we can also get a handle back to the label object, so +% that we can access its properties. + +% label axis group +h = p(2).xlabel('data value (furlongs per fortnight)'); +p(2).ylabel('normalised frequency (%)'); + +% access properties +% get(h, ... + + + +%% (d) + +% wow, those default margins suck for this figure. let's see +% if we can do better... +disp('These are the default margins - press any key to continue...'); +pause + + + +%%%% STEP 1 : TIGHT INTERNAL MARGINS + +% tighten up all internal margins to the smallest margin +% we'll use anywhere (between the un-labelled sub-grids). +% this is usually a good starting point for any layout. +p.de.margin = 2; + +% notice that we set the margin of all descendants of p, but +% the margin of p is not changed (p.de does not include p +% itself), so there is still a margin from the root panel, +% p, to the figure edge. we can display this value: +disp(sprintf('p.margin is [ %i %i %i %i ]', p.margin)); + +% the set p.fa (family) _does_ include p, so p.fa is equal +% to {p.de and p}. if you see what I mean. check help +% panel/family and help panel/descendants! you could also +% have used the line, p.fa.margin = 2, it would have worked +% just fine. + +% pause +disp('We''ve tightened internal margins - press any key to continue...'); +pause + + + +%%%% STEP 2 : INCREASE INTERNAL MARGINS AS REQUIRED + +% now, let's space out the places we want spaced out - +% remember that you can use p.identify() to get a nice +% indication of how to reference individual panels. +p(1,1).marginbottom = 12; +p(2).marginleft = 20; + +% pause +disp('We''ve increased two internal margins - press any key to continue...'); +pause + + + +%%%% STEP 3 : FINALISE MARGINS WITH FIGURE EDGES + +% finally, let's sail as close to the wind as we dare for +% the final product, by trimming the root margin to the +% bone. eliminating any wasted whitespace like this is +% particularly helpful in exported image files. +p.margin = [13 10 2 2]; + +% and let's set the global font properties, also. we can do +% this at any point, it doesn't have to be here. +p.fontsize = 8; + +% report +disp('We''ve now adjusted the figure edge margins (and reduced the fontsize), so we''re done.'); + + + + + diff --git a/lib/panel-2.14/demo/demopanelA.m b/lib/panel-2.14/demo/demopanelA.m new file mode 100644 index 00000000..8e4a5bd8 --- /dev/null +++ b/lib/panel-2.14/demo/demopanelA.m @@ -0,0 +1,128 @@ + +% Panel builds image files, not just on-screen figures. +% +% (a) Use demopanel1 to create a layout. +% (b) Export the result to file. +% (c) Export to different physical sizes. +% (d) Export at high quality. +% (e) Adjust margins. +% (f) Export using smoothing. +% (g) Export to EPS, rather than PNG. + + + +%% (a) + +% delegate +demopanel1 + +% see "help panel/export" for the full range of options. + + + +%% (b) + +% the default sizing model for export targets a piece of +% paper. the default paper model is A4, single column, with +% 20mm margins. the default aspect ratio is the golden ratio +% (landscape). therefore, if you provide only a filename, +% you get this... + +% do a default export +p.export('export_b'); + +% the default export resolution is 150DPI, so the resulting +% file will look a bit scraggy, but it's a nice small file +% that is probably fine for laying out your document. note +% that we did not supply a file extension, so PNG format is +% assumed. + + + +%% (c) + +% one thing you might want to vary from figure to +% figure is the aspect ratio. the default (golden ratio) is +% a little short, here, so let's make it a touch taller. +p.export('export_c', '-a1.4'); + +% the other thing is the column layout. we've exported to a +% single column, above - let's target a single column of a +% two-column layout. +p.export('export_c_c2', '-a1.4', '-c2'); + +% ach... that's never going to work, it doesn't fit in one +% column does it. this figure will have to span two columns, +% so let's leave it how it was. + +% NB: here, we have used the "paper sizing model". if you +% prefer, you can use the "direct sizing model" and just +% specify width and height directly. see "help +% panel/export". + + + +%% (d) + +% when you're done drafting your document, you can bring up +% the export resolution to get a nice looking figure. "-rp" +% means "publication resolution" (600DPI). +p.export('export_d', '-a1.4', '-rp'); + + + +%% (e) + +% once exported at final resolution, i can't help +% noticing the margins are a little generous. let's pull +% them in as tight as we dare to reduce the whitespace. +p.de.margin = 1; +p(1,1).marginbottom = 9; +p(2).marginleft = 12; +p.margin = [10 8 0.5 0.5]; +p.export('export_e', '-a1.4', '-rp'); + +% NB: when the margins are this tight and the output +% resolution this high, you may notice small differences in +% layout between the on-screen renderer, the PNG renderer, +% and the EPS renderer. + + + +%% (f) + +% that's now exported at 600DPI, which is fine for most +% purposes. however, the matlab renderer you are using may +% not do nice anti-aliasing like some renderers. one way to +% mitigate this is to export at a higher DPI, but that makes +% for a very large figure file. an alternative is to ask +% panel to render at a higher DPI but then to smooth back +% down to the specfied resolution. you'll have to wait a few +% seconds for the result, since rendering at these sizes +% takes time. here, we'll smooth by factor 2. since this +% takes a little while, i don't usually do this until i'm +% preparing a manuscript for submission. you can smooth by +% factor 4, but this takes even longer. +disp('rendering with smoothing, this may take some time...'); +p.export('export_f', '-a1.4', '-rp/2'); + +% NB: this is brute force smoothing, and is not the same as +% anti-aliasing. nonetheless, i find the results can be +% useful. + + + +%% (g) + +% export by default is to PNG format - other formats are +% available (see "help panel/export" for a full list). +% usually, you can set the output format just by specifying +% the file extension of the output file, as follows. +p.export('export_g.pdf', '-a1.4', '-rp'); + +% NB: if you try to export to "svg" format, panel will use +% plot2svg() if it is present. if not, you can find it at +% file exchange (http://goo.gl/VzHIR at time of writing). + + + diff --git a/lib/panel-2.14/demo/demopanelB.m b/lib/panel-2.14/demo/demopanelB.m new file mode 100644 index 00000000..24f2183d --- /dev/null +++ b/lib/panel-2.14/demo/demopanelB.m @@ -0,0 +1,42 @@ + +% Panel can incorporate an existing axis. +% +% (a) Create the root panel. +% (b) Create an axis yourself. +% (c) Pack an automatically created axis, and your own axis, +% into the root panel. + + + +%% (a) + +% create a column-pair layout, with 95% of the space given +% to the left hand panel +p = panel(); +p.pack('h', {95 []}); + +% and put an axis in the left panel +h_axis = p(1).select(); + +% and, hell, an image too +[X,Y,Z] = peaks(50); +surfc(X,Y,Z); + + + +%% (b) + +% sometimes you'll want to use some other function than +% Panel to create one or more axes. for instance, +% colorbar... +h_colorbar_axis = colorbar('peer', h_axis); + + + +%% (c) + +% panel can manage the layout of these too +p(2).select(h_colorbar_axis); + + + diff --git a/lib/panel-2.14/demo/demopanelC.m b/lib/panel-2.14/demo/demopanelC.m new file mode 100644 index 00000000..3a5f19ec --- /dev/null +++ b/lib/panel-2.14/demo/demopanelC.m @@ -0,0 +1,34 @@ + +% Recovering a Panel from a Figure. +% +% (a) Create a grid of panels, and show something in them. +% (b) Recover the root panel from the Figure. + + + +%% (a) + +% create a 2x2 grid in gcf. +clf +p = panel(); +p.pack(2, 2); + +% show dummy content +p.select('data'); + + + +%% (b) + +% say we returned from a function and didn't have a handle +% to panel - during development, it might be nice to be able +% to recover the panel from the Figure handle. we can, like +% this. if we don't pass an argument, gcf is assumed. +q = panel.recover(); + +% note that "p" and "q" now refer to the same thing - it's +% not two root panels, it's two references to the same one. +if p == q + disp('panels are identical') +end + diff --git a/lib/panel-2.14/demo/demopanelD.m b/lib/panel-2.14/demo/demopanelD.m new file mode 100644 index 00000000..d4d27beb --- /dev/null +++ b/lib/panel-2.14/demo/demopanelD.m @@ -0,0 +1,55 @@ + +% Panel can be child or parent to any graphics object. +% +% (a) Create a figure a uipanel. +% (b) Attach a panel to it. +% (c) Select another uipanel into one of the sub-panels. +% (d) Attach a callback. + + + +%% (a) + +% create the figure +clf + +% create a uipanel +set(gcf, 'units', 'normalized'); +u1 = uipanel('units', 'normalized', 'position', [0.1 0.1 0.8 0.8]); + + + +%% (b) + +% create a 2x3 grid in one of the uipanels +p = panel(u1); +p.pack(2, 3); + + + + +%% (c) + +% create another uipanel +u2 = uipanel(); + +% but let panel manage its size +p(2, 2).select(u2); + +% select all other panels in the grid as axes +p.select('data') + + + + +%% (d) + +% if you need a notification when u2 is resized, you can +% hook in to the resize event of u2. a demo callback +% function is used here, but of course you can supply any +% function handle. +someUserData = struct('whether_a_donkey_is_a_marine_mammal', false); +p(2, 2).addCallback(@demopanel_callback, someUserData); + + + diff --git a/lib/panel-2.14/demo/demopanelE.m b/lib/panel-2.14/demo/demopanelE.m new file mode 100644 index 00000000..06891bf7 --- /dev/null +++ b/lib/panel-2.14/demo/demopanelE.m @@ -0,0 +1,73 @@ + +% You can have as many root Panels as you like in one Figure. +% +% (a) Create a figure with two uipanel objects. +% (b) Attach a panel to one of these. +% (c) Attach another - oh, wait! + + + +%% (a) + +% create the figure +clf + +% create a couple of uipanels +set(gcf, 'units', 'normalized'); +u1 = uipanel('units', 'normalized', 'position', [0.1 0.1 0.35 0.8]); +u2 = uipanel('units', 'normalized', 'position', [0.55 0.1 0.35 0.8]); + + + +%% (b) + +% create a 2x2 grid in one of the uipanels +p = panel(u1); +p.pack(2, 2); +p.select('all'); + +% see? +pause(3) + + + +%% (c) + +% and, what the hell, another in the other +q = panel(u2); +q.pack(2, 2); +q.select('all'); + +% oh, wait, the first one's disappeared. why? +pause(3) + +% by default, only one panel can be attached to any one +% figure - if an existing panel is attached when you create +% another one, the existing one is first deleted. this makes +% for ease of use, usually. if you want to attach more than +% one, you have to pass the 'add' argument to the +% constructor when you create additional panels. +p = panel(u1, 'add'); +p.pack(2, 2); +p.select('all'); + +% see? +pause(3) + +% and, of course, if we try to create a new one again, once +% again without 'add', we'll delete all existing panels, as +% before... +p = panel(u1); +p.pack(2, 2); +p.select('all'); + +% see? +pause(3) + +% finally, let's show how to delete the first one, just for +% the craic. you shouldn't usually need to do this, but it +% works just fine. +delete(p); + + + diff --git a/lib/panel-2.14/demo/demopanelF.m b/lib/panel-2.14/demo/demopanelF.m new file mode 100644 index 00000000..6726b4e0 --- /dev/null +++ b/lib/panel-2.14/demo/demopanelF.m @@ -0,0 +1,49 @@ + +% You can manage fonts yourself, if you prefer. +% +% Panel, by default, manages fonts for all managed objects, +% and any associated axis labels and titles. If you want to +% manage these individually, you can turn this off by +% passing the flag "no-manage-font" to the panel +% constructor. +% +% (a) Manage fonts globally (default). +% (b) Do not manage fonts. + + + +%% (a) + +% create +figure(1) +clf +p = panel(); +p.pack(2, 2); +hh = p.select('all'); + +% create xlabels +for h = hh + xlabel(h, 'this will render as Arial', 'fontname', 'times'); +end + +% manage fonts globally +p.fontname = 'Arial'; + + + +%% (b) + +% create +figure(2) +clf +q = panel('no-manage-font'); +q.pack(2, 2); +hh = q.select('all'); + +% create xlabels +for h = hh + xlabel(h, 'this will render as Times', 'fontname', 'times'); +end + +% attempt to manage fonts globally (no effect) +q.fontname = 'Arial'; diff --git a/lib/panel-2.14/demo/demopanelG.m b/lib/panel-2.14/demo/demopanelG.m new file mode 100644 index 00000000..3a8892c2 --- /dev/null +++ b/lib/panel-2.14/demo/demopanelG.m @@ -0,0 +1,50 @@ + +% One panel can manage multiple axes/graphics objects. +% +% 19/07/12 This example, and the multi-object functionality, +% was added with release 2.5, and was suggested by user +% "Brendan" on Matlab Central. +% +% (a) Create a layout. +% (b) Create two user axes. +% (c) Have them both managed by a panel. + + + +%% (a) + +% create +clf +p = panel(); +p.pack(2, 2); + +% select sample data into some of them +p(1,1).select('data'); +p(1,2).select('data'); +p(2,1).select('data'); + + + +%% (b) + +% create two axes, one overlaying the other to provide +% separate tick labelling at top and right. + +% main axis +ax1 = axes(); + +% transparent axis for extra axis labelling +ax2 = axes('Color', 'none', 'XAxisLocation', 'top','YAxisLocation', 'Right'); + +% set up the fancy labelling (due to Brendan) +OppTickLabels = {'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k'}; +set(ax2, 'XLim', get(ax1, 'XLim'), 'YLim', get(ax1, 'YLim')); +set(ax2, 'XTick', get(ax1, 'XTick'), 'YTick', get(ax1, 'YTick')); +set(ax2, 'XTickLabel', OppTickLabels, 'YTickLabel', OppTickLabels); + + + +%% (c) + +% hand both axes to panel for position management +p(2,2).select([ax1 ax2]); diff --git a/lib/panel-2.14/demo/demopanelH.m b/lib/panel-2.14/demo/demopanelH.m new file mode 100644 index 00000000..66454cd7 --- /dev/null +++ b/lib/panel-2.14/demo/demopanelH.m @@ -0,0 +1,38 @@ + +% You can create an "inset" plot effect. +% +% 20/09/12 This example was inspired by the Matlab Central +% user "Ann Hickox". It uses absolute packing to lay +% multiple axes into the same parent panel, which is laid +% out as usual using relative packing. +% +% (a) Create the layout. +% (b) Display some data for illustration. + + +%% (a) + +% create a row of 2 panels (packed relative and horizontal) +clf +p = panel(); +p.pack('h', 2); + +% pack two absolute-packed panels into one of them +p(2).pack({[0 0 1 1]}); % main plot (fills parent) +p(2).pack({[0.67 0.67 0.3 0.3]}); % inset plot (overlaid) + +% NB: margins etc. should be applied to p(2), which is the +% parent panel of p(2, 1) (the main plot) and p(2, 2) (the +% inset). + + + +%% (b) + +% select sample data into all +p.select('data'); + +% tidy up +set(p(2, 2).axis, 'xtick', [], 'ytick', []); + + diff --git a/lib/panel-2.14/demo/demopanelI.m b/lib/panel-2.14/demo/demopanelI.m new file mode 100644 index 00000000..7b274b48 --- /dev/null +++ b/lib/panel-2.14/demo/demopanelI.m @@ -0,0 +1,98 @@ + +% Panel can fix dotted/dashed lines on export. +% +% NB: Matlab's difficulty with dotted/dashed lines on export +% seems to be fixed in R2014b, so if using this version or a +% later one, this functionality of panel will be of no +% interest. Text below was from pre R2014b. +% +% Dashed and dotted and chained lines do not render properly +% when exported to image files from Matlab, many users find. +% There are a number of solutions to this posted at file +% exchange, some of which should be compatible with Panel. +% However, for simplicity, Panel offers its own integrated +% solution, "fixdash()". Just call fixdash() with the +% handles to any lines that aren't getting rendered +% correctly at export, and cross your fingers. If you find +% conditions under which this does the wrong thing, please +% let me know. +% +% (a) Create layout. +% (b) Create a standard plot with dashed lines. +% (c) Create a similar plot and call fixdash() on the lines. +% (d) Export. +% +% RESTRICTIONS: +% +% * Does not currently work with 3D lines. This should be +% possible, but needs a bit of thought, so it'll come +% along later - nudge me at file exchange if you need it. +% +% * Currently does something a bit dumb with log plots. I +% should really fix that... + + + +%% (a) + +% create a column of 2 panels (packed relative) +clf +p = panel(); +p.pack(2); +p.margin = [10 10 2 10]; +p.de.margin = 15; + + + + +%% (b/c) + +% create a circle +th = linspace(0, 2*pi, 13); +x = cos(th) * 0.4 + 0.5; +y = sin(th) * 0.4 + 0.5; +mt = '.'; +ms = 15; +lw = 1.5; + +% for each +for pind = 1:2 + + % plot + p(pind).select(); + plot(x, y, 'k-'); + hold on + plot(x+1, y, 'r--'); + plot(x+2, y, 'g-.'); + plot(x+3, y, 'b:'); + plot(x, y+1, ['k' mt '-'], 'markersize', ms); + plot(x+1, y+1, ['r' mt '--'], 'markersize', ms); + plot(x+2, y+1, ['g' mt '-.'], 'markersize', ms); + plot(x+3, y+1, ['b' mt ':'], 'markersize', ms); + + % finalise + set(allchild(gca), 'linewidth', lw); + axis([0 5 0 2]); + + % legend + h_leg = legend('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'); + + % finalise + if pind == 2 + title('with fixdash()'); + p.fixdash([allchild(gca); allchild(h_leg)]); + else + title('without fixdash()'); + end + +end + + + +%% (d) + +% export +p.export('demopanelI.png', '-w120', '-h120', '-rp'); + + + diff --git a/lib/panel-2.14/demo/demopanelJ.m b/lib/panel-2.14/demo/demopanelJ.m new file mode 100644 index 00000000..9b1ac4d9 --- /dev/null +++ b/lib/panel-2.14/demo/demopanelJ.m @@ -0,0 +1,39 @@ + +% Panels can have fixed physical size. +% +% Panels usually have a size which is a fraction of the size +% of their parent panel (e.g. 1/3) whereas margins are of +% fixed physical size (e.g. 10mm). However, on occasion, you +% may want a panel that is of fixed physical size. This demo +% shows how to do this. +% +% (a) Create layout with one panel of fixed physical size. +% (b) Show how units affect behaviour. + + + +%% (a) + +% create a column of 2 panels (packed relative) but with +% the first one 25mm high. the fixed size is specified by +% putting the value inside {a cell array}, as in {25}, +% below. it's 25mm because the current units of p are mm (mm +% are the default unit). +clf +p = panel(); +p.pack({{25} []}); +p.select('data'); + + + +%% (b) + +% but we can change the units. +p.units = 'in'; + +% now, if we repack, the size is specified in the units +% we've chosen. this is hardly a resize - this changes it +% from 25mm to 25.4mm. +p(1).repack({1}); + + diff --git a/lib/panel-2.14/demo/demopanelK.m b/lib/panel-2.14/demo/demopanelK.m new file mode 100644 index 00000000..2fd8a3dd --- /dev/null +++ b/lib/panel-2.14/demo/demopanelK.m @@ -0,0 +1,103 @@ + +% Compare performance between Panel and subplot. +% +% If you want to see whether Panel is slow or fast on your +% machine (vs. subplot), you can use this script. +% +% (a) For each approach: +% (i) Create a grid of panels. +% (ii) Plot into each sub-panel. +% (b) Compare performance. + + + +% prepare for performance testing +close all +ss = get(0,'Screensize'); +pp = [ss(3:4)/2 + [-599 -399] 1200 800]; +figure(1) +set(gcf, 'Position', pp) +figure(2) +set(gcf, 'Position', pp) +drawnow +N = 6; +tic + +% optional stuff +optional = true; + + + +%% (a) For each approach: + +for approach = [1 2] + + % select figure + figure(approach) + + % performance + ti(approach) = toc; + + + + %% (i) + + % create a NxN grid in gcf. this is only necessary for + % panel - it is done implicitly when using subplot. + if approach == 1 + p = panel(); + p.pack(N, N); + end + + + + %% (ii) + + % plot into each panel in turn + + for m = 1:N + for n = 1:N + + % select one of the NxN grid of sub-panels + if approach == 1 + p(m, n).select(); + else + subplot(N, N, m + (n-1) * N); + end + + % optional, do some stuff + if optional + + % plot some data + plot(randn(100,1)); + + % you can use all the usual calls + xlabel('sample number'); + ylabel('data'); + + % and so on - generally, you can treat the axis panel + % like any other axis + axis([0 100 -3 3]); + + end + + end + end + + % performance + drawnow + tf(approach) = toc; + + + +end + + +%% (b) measure performance + +td = tf - ti; +fprintf('Time taken using panel: %.3f s\n', td(1)); +fprintf('Time taken using subplot: %.3f s\n', td(2)); + + + diff --git a/lib/panel-2.14/demo/demopanel_callback.m b/lib/panel-2.14/demo/demopanel_callback.m new file mode 100644 index 00000000..585fbcee --- /dev/null +++ b/lib/panel-2.14/demo/demopanel_callback.m @@ -0,0 +1,25 @@ + +% this callback is attached by demopanelD + +function demopanel_callback(data) + +disp('---- ENTER CALLBACK ----') + +% all the information is in this structure. +data +context = data.context +userdata = data.userdata + +% the "context" field provides rendering data, particularly +% the "size_in_mm" is the size of the rendering surface (the +% figure window, or an image file) whilst the "rect" is the +% rectangle assigned to this panel. therefore, we can work +% out the rendered (physical) size of this panel (and +% therefore, usually, the object it manages) with the +% following calculation. +size = data.context.size_in_mm .* data.context.rect(3:4) + +disp('---- EXIT CALLBACK ----') + + + diff --git a/lib/panel-2.14/demo/demopanel_minihist.m b/lib/panel-2.14/demo/demopanel_minihist.m new file mode 100644 index 00000000..7dfc507b --- /dev/null +++ b/lib/panel-2.14/demo/demopanel_minihist.m @@ -0,0 +1,80 @@ + +% this function is used by some of the demos to display data + +function demopanel_minihist(stats, show_xtick, show_ytick) + +% color +col = histcol(stats.source); + +% plot +b = bar(stats.bincens, stats.percfreq, 0.9); +set(b, 'facecolor', palecol(col), 'edgecolor', col, 'showbaseline', 'off'); +hold on + +% mean +x = mean(stats.values) * [1 1]; +y = [0 100]; +plot(x, y, 'k-', 'linewidth', 1); + +% label +set(gca, 'ytick', stats.ytick); +if ~show_ytick + set(gca, 'yticklabel', {}); +end + +% label +set(gca, 'xtick', stats.xtick); +if ~show_xtick + set(gca, 'xticklabel', {}); +end + +% finalise axis +axis([stats.binrange 0 stats.percpeak]); +grid on + +% overflows +N = sum(stats.values > max(stats.binrange)); +if N + y = stats.percpeak * 0.8; + x = stats.binrange(1) + [0.98] * diff(stats.binrange); + text(x, y, [int2str(N) '>'], 'hori', 'right', 'fontsize', 8); +end + +% overflows +N = sum(stats.values < min(stats.binrange)); +if N + y = stats.percpeak * 0.8; + x = stats.binrange(1) + [0.02] * diff(stats.binrange); + text(x, y, ['<' int2str(N)], 'hori', 'left', 'fontsize', 8); +end + + + + + +function col = histcol(source) + +switch source + + case 'X' + col = [1 0 0]; + + case 'Y' + col = [0 0.5 0]; + + case 'Z' + col = [0 0 1]; + +end + + + + + +function c = palecol(c) + +t = [1 1 1]; +d = t - c; +c = c + (d * 0.5); + + diff --git a/lib/panel-2.14/docs/demopanelI.png b/lib/panel-2.14/docs/demopanelI.png new file mode 100644 index 0000000000000000000000000000000000000000..b5f4010d363d0e97ede89a3816dbf25f2aae06c0 GIT binary patch literal 101545 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±z?fT6k)!62RiulF6@~LS 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