diff --git a/.github/workflows/check_markdown.yml b/.github/workflows/check_markdown.yml deleted file mode 100644 index 1b997e3d..00000000 --- a/.github/workflows/check_markdown.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Check Markdown - -on: - push: - branches: - - master - pull_request: - branches: '*' - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - - uses: actions/checkout@v2 - with: - submodules: true - fetch-depth: 1 - - - uses: actions/setup-node@v2 - with: - node-version: '10' - - - name: Install dependencies and check markdown - run: | - npm install `cat npm-requirements.txt` - npx remark *.md --frail - npx remark ./docs/ --frail - npx remark ./demos/ --frail - npx remark ./tests/ --frail diff --git a/.github/workflows/miss_hit.yml b/.github/workflows/miss_hit_quality.yml similarity index 86% rename from .github/workflows/miss_hit.yml rename to .github/workflows/miss_hit_quality.yml index 5c5e561d..9ff86d1f 100644 --- a/.github/workflows/miss_hit.yml +++ b/.github/workflows/miss_hit_quality.yml @@ -1,4 +1,4 @@ -name: miss_hit +name: miss_hit_quality on: push: @@ -29,10 +29,6 @@ jobs: python -m pip install --upgrade pip setuptools pip3 install -r requirements.txt - - name: MISS_HIT Code style - run: | - mh_style --process-slx - - name: MISS_HIT Metrics run: | mh_metric --ci diff --git a/.github/workflows/miss_hit_style.yml b/.github/workflows/miss_hit_style.yml new file mode 100644 index 00000000..17e5bdca --- /dev/null +++ b/.github/workflows/miss_hit_style.yml @@ -0,0 +1,35 @@ +name: miss_hit_style + +on: + push: + branches: + - master + pull_request: + branches: '*' + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + + - uses: actions/checkout@v2 + with: + submodules: true + fetch-depth: 1 + + - name: Set up Python 3.6 + uses: actions/setup-python@v2 + with: + python-version: 3.6 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools + pip3 install -r requirements.txt + + - name: MISS_HIT Code style + run: | + mh_style + diff --git a/.github/workflows/run_system_tests.yml b/.github/workflows/run_system_tests.yml index 88c96be0..5e19e5ff 100644 --- a/.github/workflows/run_system_tests.yml +++ b/.github/workflows/run_system_tests.yml @@ -56,20 +56,9 @@ jobs: make -C spm12/src PLATFORM=octave install octave $OCTFLAGS --eval "addpath(fullfile(pwd, 'spm12')); savepath();" - - name: Update octave path - run: | - octave $OCTFLAGS --eval "addpath(genpath(fullfile(pwd, 'lib'))); savepath();" - octave $OCTFLAGS --eval "addpath(genpath(fullfile(pwd, 'src'))); savepath();" - - - 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 - + - name: Run system tests run: | - cd demos/MoAE - octave $OCTFLAGS --eval "MoAEpilot_run" + cd manualTests/ + octave $OCTFLAGS --eval "test_moae" diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index f45c7522..b9f64864 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -62,22 +62,23 @@ 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: | - 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 .. - name: Run tests run: | - octave $OCTFLAGS --eval "runTests" + octave $OCTFLAGS --eval "run_tests" cat test_report.log | grep 0 bash <(curl -s https://codecov.io/bash) diff --git a/.gitignore b/.gitignore index 910d5fa3..f0a8a647 100644 --- a/.gitignore +++ b/.gitignore @@ -13,27 +13,23 @@ options_task-*date*.json 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/*/*.nii +demos/*/cfg/*.json # test folder and dummy data -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/* # ignore virtual env cpp_spm/* +env/* # visual studio code stuff .vscode diff --git a/.gitmodules b/.gitmodules index 4fd1373b..b6c5a042 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,12 @@ [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 +[submodule "lib/slice_display"] + path = lib/slice_display + url = https://github.com/bramzandbelt/slice_display.git +[submodule "lib/brain_colours"] + path = lib/brain_colours + url = https://github.com/CPernet/brain_colours.git diff --git a/.remarkrc b/.remarkrc deleted file mode 100644 index 201ce70e..00000000 --- a/.remarkrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "plugins": [ - "preset-lint-consistent", - "preset-lint-markdown-style-guide", - "preset-lint-recommended", - ["lint-no-duplicate-headings", false], - ["lint-list-item-indent", "tab-size"], - ["lint-maximum-line-length", true], - ["lint-maximum-heading-length", false] - ] -} 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/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 88b79e16..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,346 +0,0 @@ -# Contributing - -## Unit testing - -All tests are in the test folder. There is also an empty dummy BIDS dataset that -is partly created using the bash script `createDummyDataSet.sh`. - -## Changelog - - - - diff --git a/README.md b/README.md index b9ed94b4..681cf30e 100644 --- a/README.md +++ b/README.md @@ -21,16 +21,69 @@ **Contributors** + [![All Contributors](https://img.shields.io/badge/all_contributors-8-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 + + + +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,32 +108,19 @@ 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 +## Octave compatibility -### Dependencies +The following features do not yet work with Octave: -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 | - - +- anatomicalQA +- functionalQA +- slice_display toolbox ## Contributing @@ -93,6 +133,18 @@ or you get stuck: it is more likely we did not do good enough a job at explaining things. So do not hesitate to open an issue, just to ask for clarification. +### Style guidelines + +These are some of the guidelines we try to follow. + +We use `camelCase` to name functions and variables for the vast majority of the +code in this repository. + +Scripts names in general and as well functions related to the demos use a +`snake_case`. + +Constant are written in `UPPERCASE`. + ## Contributors Thanks goes to these wonderful people diff --git a/demos/MoAE/MoAEpilot_run.m b/demos/MoAE/MoAEpilot_run.m deleted file mode 100644 index 0d56f37b..00000000 --- a/demos/MoAE/MoAEpilot_run.m +++ /dev/null @@ -1,61 +0,0 @@ -% (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. -% 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) - -clear; -clc; - -% 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')); - -% we add all the subfunctions that are in the sub directories -addpath(genpath(fullfile(WD, '..', '..', 'src'))); -addpath(genpath(fullfile(WD, '..', '..', 'lib'))); - -%% 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'); - -checkDependencies(); - -%% 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); - -bidsSTC(opt); - -bidsSpatialPrepro(opt); - -% The following do not run on octave for now (because of spmup) -anatomicalQA(opt); -bidsResliceTpmToFunc(opt); -functionalQA(opt); - -bidsSmoothing(FWHM, opt); - -% The following crash on Travis CI -% bidsFFX('specifyAndEstimate', opt, FWHM); -% bidsFFX('contrasts', opt, FWHM); -% bidsResults(opt, FWHM); diff --git a/demos/MoAE/download_moae_ds.m b/demos/MoAE/download_moae_ds.m new file mode 100644 index 00000000..03fc0621 --- /dev/null +++ b/demos/MoAE/download_moae_ds.m @@ -0,0 +1,31 @@ +function download_moae_ds(downloadData) + % + % (C) Copyright 2021 Remi Gau + + 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 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..accf97bd --- /dev/null +++ b/demos/MoAE/moae_create_roi_extract_data.m @@ -0,0 +1,59 @@ +% (C) Copyright 2021 Remi Gau + +%% 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 moae_run.m; + +subLabel = '01'; + +saveROI = true; + +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$']); + +%% Create ROI right auditory cortex +sphere.location = [57 -22 11]; + +specification = struct( ... + 'mask1', maskImage, ... + 'mask2', sphere); + +[~, roiFile] = createRoi('expand', specification, conImage, pwd, saveROI); + +% rename mask image +newname.desc = 'right auditory cortex'; +newname.task = ''; +newname.label = ''; +newname.p = ''; +newname.k = ''; +newname.MC = ''; +rightRoiFile = renameFile(roiFile, newname); + +%% same but with left hemisphere +sphere.location = [-57 -22 11]; + +specification = struct( ... + 'mask1', maskImage, ... + 'mask2', sphere); + +[~, roiFile] = createRoi('expand', specification, conImage, pwd, saveROI); +newname.desc = 'left auditory cortex'; +leftRoiFile = renameFile(roiFile, newname); + +%% +right_data = spm_summarise(conImage, rightRoiFile); +left_data = spm_summarise(conImage, leftRoiFile); diff --git a/demos/MoAE/MoAEpilot_getOption.m b/demos/MoAE/moae_get_option.m similarity index 88% rename from demos/MoAE/MoAEpilot_getOption.m rename to demos/MoAE/moae_get_option.m index fc448180..40110173 100644 --- a/demos/MoAE/MoAEpilot_getOption.m +++ b/demos/MoAE/moae_get_option.m @@ -1,8 +1,9 @@ -% (C) Copyright 2019 Remi Gau - -function opt = MoAEpilot_getOption() +function opt = moae_get_option() + % % returns a structure that contains the options chosen by the user to run % slice timing correction, pre-processing, FFX, RFX. + % + % (C) Copyright 2019 Remi Gau opt = []; @@ -10,8 +11,8 @@ 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'); + opt.derivativesDir = fullfile(opt.dataDir, '..', '..', 'outputs'); % Uncomment the lines below to run preprocessing % - don't use realign and unwarp @@ -56,7 +57,7 @@ % MONTAGE FIGURE OPTIONS opt.result.Steps(1).Output.montage.do = true(); - opt.result.Steps(1).Output.montage.slices = -8:3:15; % in mm + opt.result.Steps(1).Output.montage.slices = -0:2:16; % in mm % axial is default 'sagittal', 'coronal' opt.result.Steps(1).Output.montage.orientation = 'axial'; % will use the MNI T1 template by default but the underlay image can be changed. diff --git a/demos/MoAE/moae_run.m b/demos/MoAE/moae_run.m new file mode 100644 index 00000000..4d4d71b0 --- /dev/null +++ b/demos/MoAE/moae_run.m @@ -0,0 +1,52 @@ +% (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. +% +% 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) + +clear; +clc; + +% Smoothing to apply +FWHM = 6; + +downloadData = true; + +run ../../initCppSpm.m; + +%% Set options +opt = moae_get_option(); + +download_moae_ds(downloadData); + +%% Run batches +reportBIDS(opt); +bidsCopyRawFolder(opt, 1); + +% In case you just want to run segmentation and skull stripping +% bidsSegmentSkullStrip(opt); +% +% NOTE: skull stripping is also included in 'bidsSpatialPrepro' + +bidsSTC(opt); + +bidsSpatialPrepro(opt); + +% The following do not run on octave for now (because of spmup) +anatomicalQA(opt); +bidsResliceTpmToFunc(opt); +functionalQA(opt); + +bidsSmoothing(FWHM, opt); + +% The following crash on CI +WD = pwd; +bidsFFX('specifyAndEstimate', opt, FWHM); +cd(WD); +bidsFFX('contrasts', opt, FWHM); +cd(WD); +bidsResults(opt, FWHM); +cd(WD); diff --git a/demos/MoAE/moae_slice_display.m b/demos/MoAE/moae_slice_display.m new file mode 100644 index 00000000..a0662caa --- /dev/null +++ b/demos/MoAE/moae_slice_display.m @@ -0,0 +1,60 @@ +% (C) Copyright 2021 Remi Gau + +run moae_create_roi_extract_data.m; + +close all; + +%% Layers +layers = sd_config_layers('init', {'truecolor', 'dual', 'contour', 'contour'}); + +% Layer 1: Anatomical map +[anat_normalized_file, anatRange] = return_normalized_anat_file(opt, subLabel); +layers(1).color.file = anat_normalized_file; +layers(1).color.range = [0 anatRange(2)]; + +layers(1).color.map = gray(256); + +%% Layer 2: Dual-coded layer +% +% - contrast estimates color-coded; + +layers(2).color.file = conImage; + +color_map_folder = fullfile(fileparts(which('map_luminance')), '..', 'mat_maps'); +load(fullfile(color_map_folder, 'diverging_bwr_iso.mat')); +layers(2).color.map = diverging_bwr; + +layers(2).color.range = [-4 4]; +layers(2).color.label = '\beta_{listening} - \beta_{baseline} (a.u.)'; + +%% Layer 2: Dual-coded layer +% +% - t-statistics opacity-coded + +spmTImage = spm_select('FPList', ffxDir, ['^spmT_' p.label '.nii$']); +layers(2).opacity.file = spmTImage; + +layers(2).opacity.range = [2 3]; +layers(2).opacity.label = '| t |'; + +%% Layer 3 and 4: Contour of ROI + +layers(3).color.file = rightRoiFile; +layers(3).color.map = [0 0 0]; +layers(3).color.line_width = 2; + +layers(4).color.file = leftRoiFile; +layers(4).color.map = [1 1 1]; +layers(4).color.line_width = 2; + +%% Settings +settings = sd_config_settings('init'); + +% we reuse the details for the SPM montage +settings.slice.orientation = opt.result.Steps(1).Output.montage.orientation; +settings.slice.disp_slices = opt.result.Steps(1).Output.montage.slices; +settings.fig_specs.n.slice_column = 3; +settings.fig_specs.title = opt.result.Steps(1).Contrasts(1).Name; + +%% Display the layers +[settings, p] = sd_display(layers, settings); 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/demos/MoAE/options_task-auditory.json b/demos/MoAE/options/options_task-auditory.json similarity index 64% rename from demos/MoAE/options_task-auditory.json rename to demos/MoAE/options/options_task-auditory.json index 00982156..198477b0 100644 --- a/demos/MoAE/options_task-auditory.json +++ b/demos/MoAE/options/options_task-auditory.json @@ -1,19 +1,12 @@ { - "STC_referenceSlice": [], "anatReference": { "type": "T1w", - "session": 1 + "session": "" }, - "contrastList": [], - "dataDir": "/home/remi/github/CPP_SPM/demos/MoAE/output/MoAEpilot", - "derivativesDir": "/home/remi/github/CPP_SPM/demos/MoAE/derivatives/default", - "funcVoxelDims": [], - "groups": [""], + "dataDir": "./inputs/raw", + "derivativesDir": "./outputs/", "model": { - "file": "/home/remi/github/CPP_SPM/demos/MoAE/models/model-MoAE_smdl.json" - }, - "realign": { - "useUnwarp": true + "file": "./models/model-MoAE_smdl.json" }, "result": { "Steps": { @@ -41,9 +34,7 @@ "skullstrip": { "threshold": 0.75 }, - "sliceOrder": [], "space": "MNI", - "subjects": [[]], "taskName": "auditory", "useFieldmaps": true, "zeropad": 2 diff --git a/demos/MoAE/options_task-auditory_space-individual.json b/demos/MoAE/options/options_task-auditory_space-individual.json similarity index 71% rename from demos/MoAE/options_task-auditory_space-individual.json rename to demos/MoAE/options/options_task-auditory_space-individual.json index 346b482b..4abd8a40 100644 --- a/demos/MoAE/options_task-auditory_space-individual.json +++ b/demos/MoAE/options/options_task-auditory_space-individual.json @@ -1,15 +1,12 @@ { - "STC_referenceSlice": [], "anatReference": { "type": "T1w", - "session": 1 + "session": "" }, - "contrastList": [], - "dataDir": "/home/remi/github/CPP_SPM/demos/MoAE/output/MoAEpilot", - "derivativesDir": "/home/remi/github/CPP_SPM/demos/MoAE/derivatives/native", - "funcVoxelDims": [], + "dataDir": "./inputs/raw", + "derivativesDir": "./outputs/", "model": { - "file": "/home/remi/github/CPP_SPM/demos/MoAE/models/model-MoAE_smdl.json" + "file": "./models/model-MoAE_smdl.json" }, "realign": { "useUnwarp": true @@ -40,7 +37,6 @@ "skullstrip": { "threshold": 0.75 }, - "sliceOrder": [], "space": "individual", "taskName": "auditory", "useFieldmaps": true, diff --git a/demos/MoAE/options_task-auditory_unwarp-0.json b/demos/MoAE/options/options_task-auditory_unwarp-0.json similarity index 71% rename from demos/MoAE/options_task-auditory_unwarp-0.json rename to demos/MoAE/options/options_task-auditory_unwarp-0.json index 009b8147..8430077f 100644 --- a/demos/MoAE/options_task-auditory_unwarp-0.json +++ b/demos/MoAE/options/options_task-auditory_unwarp-0.json @@ -1,15 +1,12 @@ { - "STC_referenceSlice": [], "anatReference": { "type": "T1w", - "session": 1 + "session": "" }, - "contrastList": [], - "dataDir": "/home/remi/github/CPP_SPM/demos/MoAE/output/MoAEpilot", - "derivativesDir": "/home/remi/github/CPP_SPM/demos/MoAE/derivatives/unwarp-0", - "funcVoxelDims": [], + "dataDir": "./inputs/raw", + "derivativesDir": "./outputs/", "model": { - "file": "/home/remi/github/CPP_SPM/demos/MoAE/models/model-MoAE_smdl.json" + "file": "./models/model-MoAE_smdl.json" }, "realign": { "useUnwarp": false @@ -40,7 +37,6 @@ "skullstrip": { "threshold": 0.75 }, - "sliceOrder": [], "space": "MNI", "taskName": "auditory", "useFieldmaps": true, diff --git a/demos/MoAE/options_task-auditory_unwarp-0_space-individual.json b/demos/MoAE/options/options_task-auditory_unwarp-0_space-individual.json similarity index 70% rename from demos/MoAE/options_task-auditory_unwarp-0_space-individual.json rename to demos/MoAE/options/options_task-auditory_unwarp-0_space-individual.json index 6dd7198f..2d1a17e5 100644 --- a/demos/MoAE/options_task-auditory_unwarp-0_space-individual.json +++ b/demos/MoAE/options/options_task-auditory_unwarp-0_space-individual.json @@ -1,15 +1,12 @@ { - "STC_referenceSlice": [], "anatReference": { "type": "T1w", - "session": 1 + "session": "" }, - "contrastList": [], - "dataDir": "/home/remi/github/CPP_SPM/demos/MoAE/output/MoAEpilot", - "derivativesDir": "/home/remi/github/CPP_SPM/demos/MoAE/derivatives/unwarp-0_native", - "funcVoxelDims": [], + "dataDir": "./inputs/raw", + "derivativesDir": "./outputs/", "model": { - "file": "/home/remi/github/CPP_SPM/demos/MoAE/models/model-MoAE_smdl.json" + "file": "./models/model-MoAE_smdl.json" }, "realign": { "useUnwarp": false @@ -40,7 +37,6 @@ "skullstrip": { "threshold": 0.75 }, - "sliceOrder": [], "space": "individual", "taskName": "auditory", "useFieldmaps": true, diff --git a/demos/MoAE/return_normalized_anat_file.m b/demos/MoAE/return_normalized_anat_file.m new file mode 100644 index 00000000..785543dc --- /dev/null +++ b/demos/MoAE/return_normalized_anat_file.m @@ -0,0 +1,16 @@ +function [anat_normalized_file, anatRange] = return_normalized_anat_file(opt, subLabel) + % + % (C) Copyright 2021 Remi Gau + + [BIDS, opt] = getData(opt); + [~, anatDataDir] = getAnatFilename(BIDS, subLabel, opt); + anat_normalized_file = spm_select('FPList', ... + anatDataDir, ... + '^wm.*skullstripped.nii$'); + + hdr = spm_vol(anat_normalized_file); + vol = spm_read_vols(hdr); + + anatRange = [min(vol(:)) max(vol(:))]; + +end diff --git a/demos/spm_face_rep/face_rep_convert2BIDS.m b/demos/face_repetition/download_convert_face_rep_ds.m similarity index 76% rename from demos/spm_face_rep/face_rep_convert2BIDS.m rename to demos/face_repetition/download_convert_face_rep_ds.m index fffea5da..72a901f7 100644 --- a/demos/spm_face_rep/face_rep_convert2BIDS.m +++ b/demos/face_repetition/download_convert_face_rep_ds.m @@ -1,12 +1,11 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - -function face_rep_convert2BIDS() +function download_convert_face_rep_ds() % - % downloads the fare repetition dataset from SPM and convert it to BIDS + % downloads the face repetition dataset from SPM and convert it to BIDS % % Adapted from its counterpart for MoAE % % + % (C) Copyright 2020 CPP_SPM developers subject = 'sub-01'; task_name = 'face repetition'; @@ -19,8 +18,18 @@ 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')); + 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'); + catch + end + spm_mkdir(fullfile(working_directory, 'inputs')); + spm_mkdir(output_dir); %% Get data fprintf('%-10s:', 'Downloading dataset...'); @@ -28,46 +37,46 @@ function face_rep_convert2BIDS() fprintf(1, ' Done\n\n'); fprintf('%-10s:', 'Unzipping dataset...'); - unzip('face_rep.zip', WD); - 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(WD, 'raw', subject, {'anat', 'func'}); + %% Create ouput folder structure + spm_mkdir(output_dir, subject, {'anat', 'func'}); %% Structural MRI - anat_hdr = spm_vol(fullfile(WD, '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(WD, '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(WD, 'source', 'RawEPI'), '^sM.*\.img$'); + func_files = spm_select('FPList', fullfile(input_dir, 'RawEPI'), '^sM.*\.img$'); spm_file_merge( ... func_files, ... - fullfile(WD, 'raw', 'sub-01', 'func', ... + fullfile(output_dir, 'sub-01', 'func', ... ['sub-01_task-' strrep(task_name, ' ', '') '_bold.nii']), ... 0, ... repetition_time); - delete(fullfile(WD, '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(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(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(WD, 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(WD, 'source', 'all_conditions.mat'), ... + load(fullfile(input_dir, 'all_conditions.mat'), ... 'names', 'onsets', 'durations'); onset_column = []; @@ -94,13 +103,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(output_dir, 'sub-01', 'func', ... ['sub-01_task-' strrep(task_name, ' ', '') '_events.tsv']), ... tsv_content); end -function create_readme(WD) +function create_readme(output_dir) rdm = { ' ___ ____ __ __' @@ -152,7 +161,7 @@ function create_readme(WD) % TODO % use spm_save to actually write this file? - fid = fopen(fullfile(WD, 'raw', 'README'), 'wt'); + fid = fopen(fullfile(output_dir, 'README'), 'wt'); for i = 1:numel(rdm) fprintf(fid, '%s\n', rdm{i}); end @@ -160,12 +169,12 @@ function create_readme(WD) end -function create_changelog(WD) +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(WD, 'raw', 'CHANGES'), 'wt'); + fid = fopen(fullfile(output_dir, 'CHANGES'), 'wt'); for i = 1:numel(cg) fprintf(fid, '%s\n', cg{i}); @@ -174,7 +183,7 @@ function create_changelog(WD) end -function create_datasetdescription(WD, opt) +function create_datasetdescription(output_dir, opt) descr = struct( ... 'BIDSVersion', '1.4.0', ... @@ -193,13 +202,13 @@ function create_datasetdescription(WD, opt) 'doi:10.1093/cercor/12.2.178'}} ... ); - spm_save(fullfile(WD, 'raw', 'dataset_description.json'), ... + spm_save(fullfile(output_dir, 'dataset_description.json'), ... descr, ... opt); end -function create_bold_json(WD, 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); @@ -219,9 +228,7 @@ function create_bold_json(WD, task_name, repetition_time, nb_slices, echo_time, 'ManufacturersModelName', 'MAGNETOM Vision', ... 'MagneticFieldStrength', 2); - spm_save(fullfile( ... - WD, ... - 'raw', ... + spm_save(fullfile(output_dir, ... ['task-' strrep(task_name, ' ', '') '_bold.json']), ... task, ... opt); diff --git a/demos/face_repetition/face_rep_anat.m b/demos/face_repetition/face_rep_anat.m new file mode 100644 index 00000000..eac2850b --- /dev/null +++ b/demos/face_repetition/face_rep_anat.m @@ -0,0 +1,34 @@ +% (C) Copyright 2019 Remi Gau +% +% +% This show how an anat only workflow would look like +% + +clear; +clc; + +downloadData = true; + +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 + + download_convert_face_rep_ds(); + +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/face_repetition/face_rep_func.m similarity index 63% rename from demos/spm_face_rep/face_rep_run.m rename to demos/face_repetition/face_rep_func.m index 5ea3695e..b8817171 100644 --- a/demos/spm_face_rep/face_rep_run.m +++ b/demos/face_repetition/face_rep_func.m @@ -1,5 +1,5 @@ % (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. % @@ -17,37 +17,22 @@ clear; clc; -% Smoothing to apply -FWHM = 8; +FWHM = 6; -DownloadData = true; +downloadData = true; -% URL of the data set to download -% 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; %% Set options -opt = FaceRep_getOption(); +opt = face_rep_get_option(); %% Removes previous analysis, gets data and converts it to BIDS -if DownloadData - try %#ok<*UNRCH> - rmdir('source', 's'); - rmdir('raw', 's'); - catch - end +if downloadData - face_rep_convert2BIDS(); + download_convert_face_rep_ds(); end -%% -checkDependencies(); - %% Run batches reportBIDS(opt); bidsCopyRawFolder(opt, 1); @@ -66,8 +51,9 @@ bidsSmoothing(FWHM, opt); % The following crash on Travis CI +opt.dir.stats = opt.derivativesDir; bidsFFX('specifyAndEstimate', opt, FWHM); bidsFFX('contrasts', opt, FWHM); % TODO -bidsResults(opt, FWHM); +% bidsResults(opt, FWHM); diff --git a/demos/spm_face_rep/FaceRep_getOption.m b/demos/face_repetition/face_rep_get_option.m similarity index 55% rename from demos/spm_face_rep/FaceRep_getOption.m rename to demos/face_repetition/face_rep_get_option.m index c20e020a..ea0293e3 100644 --- a/demos/spm_face_rep/FaceRep_getOption.m +++ b/demos/face_repetition/face_rep_get_option.m @@ -1,16 +1,16 @@ -% (C) Copyright 2019 Remi Gau - -function opt = FaceRep_getOption() +function opt = face_rep_get_option() + % % returns a structure that contains the options chosen by the user to run % slice timing correction, pre-processing, FFX, RFX. + % + % (C) Copyright 2020 Remi Gau opt = []; - % task to analyze 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.dir.roi = fullfile(opt.dataDir, '..', 'derivatives', 'cpp_spm-roi'); opt.model.hrfDerivatives = [1 1]; diff --git a/demos/face_repetition/face_rep_get_option_results.m b/demos/face_repetition/face_rep_get_option_results.m new file mode 100644 index 00000000..8b36c80c --- /dev/null +++ b/demos/face_repetition/face_rep_get_option_results.m @@ -0,0 +1,36 @@ +function opt = face_rep_get_option_results() + % + % (C) Copyright 2021 Remi Gau + + opt = face_rep_get_option(); + + opt.model.file = fullfile( ... + fileparts(mfilename('fullpath')), ... + 'models', ... + 'model-faceRepetition_smdl.json'); + + opt.glm.QA.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 + opt.result.Steps(1).Output.montage.orientation = 'axial'; + +end diff --git a/demos/face_repetition/face_rep_resolution.m b/demos/face_repetition/face_rep_resolution.m new file mode 100644 index 00000000..38461e17 --- /dev/null +++ b/demos/face_repetition/face_rep_resolution.m @@ -0,0 +1,80 @@ +% (C) Copyright 2019 Remi Gau +% +% +% runs preprocessing with different final spatial resolution in MNI space +% + +clear; +clc; +close all; + +FWHM = 6; + +downloadData = true; + +run ../../initCppSpm.m; + +%% Set options + +opt = face_rep_get_option_results(); + +%% Removes previous analysis, gets data and converts it to BIDS +if downloadData + + download_convert_face_rep_ds(); + +end + +%% Run batches + +reportBIDS(opt); + +modelFile = opt.model.file; + +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'); + + % create a new BIDS model json file + % this way the GLM output will be store in a different directory for each + % resolution as the name of the GLM directory is based on the name of the + % model in the BIDS model + content = spm_jsonread(opt.model.file); + content.Name = [content.Name, ' resolution - ', num2str(iResolution)]; + + p = bids.internal.parse_filename(modelFile); + p.model = [p.model, ' resolution', num2str(iResolution)]; + newModel = spm_file(opt.model.file, 'filename', createFilename(p)); + opt.model.file = newModel; + + spm_jsonwrite(newModel, content, struct('indent', ' ')); + + % run analysis + 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 diff --git a/demos/face_repetition/face_rep_roi_analysis.m b/demos/face_repetition/face_rep_roi_analysis.m new file mode 100644 index 00000000..4799bd3c --- /dev/null +++ b/demos/face_repetition/face_rep_roi_analysis.m @@ -0,0 +1,21 @@ +% (C) Copyright 2019 Remi Gau +% +% creates a ROI in MNI space from the proba atlas +% creates its equivalent in subject space +% + +run face_rep_anat.m; + +opt = face_rep_get_option(); + +opt.roi.atlas = 'wang'; +opt.roi.name = {'V1v', 'V1d'}; +opt.roi.space = {'MNI', 'individual'}; + +opt.dir.stats = fullfile(opt.dataDir, '..', 'derivatives', 'cpp_spm-stats'); + +bidsCreateROI(opt); + +opt.glm.roibased.do = true; + +bidsRoiBasedGLM(opt); diff --git a/demos/face_repetition/models/model-faceRepetition_smdl.json b/demos/face_repetition/models/model-faceRepetition_smdl.json new file mode 100644 index 00000000..29fd68f1 --- /dev/null +++ b/demos/face_repetition/models/model-faceRepetition_smdl.json @@ -0,0 +1,74 @@ +{ + "Name": "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" + } + ] + } + ] +} \ No newline at end of file diff --git a/demos/lesion_detection/batch_lesion.m b/demos/lesion_detection/batch_lesion.m new file mode 100644 index 00000000..87de2b2b --- /dev/null +++ b/demos/lesion_detection/batch_lesion.m @@ -0,0 +1,20 @@ +% (C) Copyright 2021 CPP_SPM developers + +clear; +clc; + +% URL of the data set to download +% URL = https://gin.g-node.org/mwmaclean/CVI-Datalad/src/master/data + +run ../../initCppSpm.m; + +%% Get Data +opt = Lesion_getOption(); + +%% Run batches +reportBIDS(opt); + +deleteZippedNii = true; +bidsCopyRawFolder(opt, deleteZippedNii, {'anat'}); + +bidsLesionSegmentation(opt); diff --git a/demos/lesion_detection/lesion_get_option.m b/demos/lesion_detection/lesion_get_option.m new file mode 100644 index 00000000..2da9718f --- /dev/null +++ b/demos/lesion_detection/lesion_get_option.m @@ -0,0 +1,21 @@ +function opt = lesion_get_option() + % + % 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) + % + % (C) Copyright 2021 CPP_SPM developers + + % The directory where the data are located + opt.dataDir = '/home/remi/gin/CVI-Datalad/data'; + + %% DO NOT TOUCH + opt = checkOptions(opt); + saveOptions(opt); + +end diff --git a/demos/miss_hit.cfg b/demos/miss_hit.cfg index 25947f2d..007bd432 100644 --- a/demos/miss_hit.cfg +++ b/demos/miss_hit.cfg @@ -1,12 +1 @@ -# styly guide (https://florianschanda.github.io/miss_hit/style_checker.html) -line_length: 100 -regex_function_name: "[a-zA-Z0-9]+(_*([a-zA-Z0-9]){1}[A-Za-z]+)*" # almost anything goes in the root folder -copyright_entity: "Mohamed Rezk" -copyright_entity: "Remi Gau" -copyright_entity: "CPP BIDS SPM-pipeline developpers" - -# metrics limit for the code quality (https://florianschanda.github.io/miss_hit/metrics.html) -metric "cnest": limit 4 -metric "file_length": limit 400 -metric "cyc": limit 12 -metric "parameters": limit 6 \ No newline at end of file +regex_function_name: "[a-z0-9]+(_[a-z0-9]+)*" \ No newline at end of file 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/demos/openneuro/ds000001_getOption.m b/demos/openneuro/ds000001_get_option.m similarity index 93% rename from demos/openneuro/ds000001_getOption.m rename to demos/openneuro/ds000001_get_option.m index b468ef5b..8d9daa1b 100644 --- a/demos/openneuro/ds000001_getOption.m +++ b/demos/openneuro/ds000001_get_option.m @@ -1,8 +1,9 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - -function opt = ds000001_getOption() +function opt = ds000001_get_option() + % % returns a structure that contains the options chosen by the user to run % slice timing correction, pre-processing, FFX, RFX. + % + % (C) Copyright 2020 CPP_SPM developers if nargin < 1 opt = []; diff --git a/demos/openneuro/ds000001_run.m b/demos/openneuro/ds000001_run.m index 38d9a18e..a0d11fab 100644 --- a/demos/openneuro/ds000001_run.m +++ b/demos/openneuro/ds000001_run.m @@ -1,4 +1,4 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers +% (C) Copyright 2020 CPP_SPM developers % runDs00014 @@ -9,17 +9,10 @@ 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'))); +run ../../initCppSpm.m; %% Set options -opt = ds000001_getOption(); - -checkDependencies(); +opt = ds000001_get_option(); reportBIDS(opt); diff --git a/demos/openneuro/ds000114_getOption.m b/demos/openneuro/ds000114_get_option.m similarity index 92% rename from demos/openneuro/ds000114_getOption.m rename to demos/openneuro/ds000114_get_option.m index 5797314e..2999d75a 100644 --- a/demos/openneuro/ds000114_getOption.m +++ b/demos/openneuro/ds000114_get_option.m @@ -1,8 +1,10 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - -function opt = ds000114_getOption() +function opt = ds000114_get_option() + % % returns a structure that contains the options chosen by the user to run % slice timing correction, pre-processing, FFX, RFX. + % + % + % (C) Copyright 2020 CPP_SPM developers if nargin < 1 opt = []; @@ -18,7 +20,7 @@ opt.taskName = 'linebisection'; opt.anatReference.type = 'T1w'; - opt.anatReference.session = 2; + opt.anatReference.session = 'retest'; % Uncomment the lines below to run preprocessing % - don't use realign and unwarp diff --git a/demos/openneuro/ds000114_run.m b/demos/openneuro/ds000114_run.m index 0c72597d..f3d8dd1d 100644 --- a/demos/openneuro/ds000114_run.m +++ b/demos/openneuro/ds000114_run.m @@ -1,4 +1,4 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers +% (C) Copyright 2020 CPP_SPM developers % runDs00014 @@ -9,17 +9,10 @@ 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'))); +run ../../initCppSpm.m; %% Set options -opt = ds000114_getOption(); - -checkDependencies(); +opt = ds000114_get_option(); %% Run batches diff --git a/demos/openneuro/ds001168_getOption.m b/demos/openneuro/ds001168_get_option.m similarity index 84% rename from demos/openneuro/ds001168_getOption.m rename to demos/openneuro/ds001168_get_option.m index 6a6ebfb4..7961bad5 100644 --- a/demos/openneuro/ds001168_getOption.m +++ b/demos/openneuro/ds001168_get_option.m @@ -1,8 +1,10 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - -function opt = ds001168_getOption() +function opt = ds001168_get_option() + % % returns a structure that contains the options chosen by the user to run % slice timing correction, pre-processing, FFX, RFX. + % + % + % (C) Copyright 2020 CPP_SPM developers if nargin < 1 opt = []; @@ -18,7 +20,7 @@ opt.taskName = 'rest'; opt.anatReference.type = 'T1w'; - opt.anatReference.session = 1; + opt.anatReference.session = '1'; % Uncomment the lines below to run preprocessing % - don't use realign and unwarp diff --git a/demos/openneuro/ds001168_run.m b/demos/openneuro/ds001168_run.m index e8ab780c..695b3b6d 100644 --- a/demos/openneuro/ds001168_run.m +++ b/demos/openneuro/ds001168_run.m @@ -1,4 +1,4 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers +% (C) Copyright 2020 CPP_SPM developers % runDs001168 @@ -8,17 +8,10 @@ % 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'))); +run ../../initCppSpm.m; %% Set options -opt = ds001168_getOption(); - -checkDependencies(); +opt = ds001168_get_option(); %% Run batches 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/demos/vismotion/batch.m b/demos/vismotion/batch.m index 8c52b50d..b88ef07b 100644 --- a/demos/vismotion/batch.m +++ b/demos/vismotion/batch.m @@ -1,4 +1,4 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers +% (C) Copyright 2019 CPP_SPM developers clear; clc; @@ -6,14 +6,10 @@ % 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(); +opt = get_option(); reportBIDS(opt); diff --git a/demos/vismotion/getOption.m b/demos/vismotion/get_option.m similarity index 84% rename from demos/vismotion/getOption.m rename to demos/vismotion/get_option.m index dca50692..1671e4f7 100644 --- a/demos/vismotion/getOption.m +++ b/demos/vismotion/get_option.m @@ -1,9 +1,11 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - -function opt = getOption() - % opt = getOption() +function opt = get_option() + % % returns a structure that contains the options chosen by the user to run % slice timing correction, pre-processing, FFX, RFX. + % + % opt = get_option() + % + % (C) Copyright 2019 CPP_SPM developers if nargin < 1 opt = []; @@ -17,8 +19,7 @@ opt.derivativesDir = '/home/remi/Documents'; % specify the model file that contains the contrasts to compute - opt.model.file = ... - '/home/remi/github/CPP_BIDS_SPM_pipeline/demos/vismotion/models/model-visMotionLoc_smdl.json'; + opt.model.file = './vismotion/models/model-visMotionLoc_smdl.json'; % specify the result to compute % Contrasts.Name has to match one of the contrast defined in the model json file diff --git a/docs/README.md b/docs/README.md index 4f1ca4e7..add9f199 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,8 +3,8 @@ ## Set up virtual environment ```bash -virtualenv -p python3 cpp_spm -source cpp_spm/bin/activate +virtualenv -p /usr/bin/python3 env +source env/bin/activate pip install -r requirements.txt ``` diff --git a/docs/source/batches.rst b/docs/source/batches.rst index 9da85ccb..86076033 100644 --- a/docs/source/batches.rst +++ b/docs/source/batches.rst @@ -7,25 +7,35 @@ List of functions to set SPM batches. .. automodule:: src.batches +.. autofunction:: setBatch3Dto4D .. autofunction:: setBatchComputeVDM +.. autofunction:: setBatchContrasts .. autofunction:: setBatchCoregistrationFmap .. autofunction:: setBatchCoregistrationFuncToAnat .. autofunction:: setBatchCoregistration .. autofunction:: setBatchCreateVDMs +.. autofunction:: setBatchEstimateModel .. autofunction:: setBatchFactorialDesign +.. autofunction:: setBatchGroupLevelContrasts .. autofunction:: setBatchImageCalculation +.. autofunction:: setBatchLesionAbnormalitiesDetection +.. autofunction:: setBatchLesionOverlapMap +.. autofunction:: setBatchLesionSegmentation .. autofunction:: setBatchMeanAnatAndMask .. autofunction:: setBatchNormalizationSpatialPrepro .. autofunction:: setBatchNormalize +.. autofunction:: setBatchPrintFigure .. autofunction:: setBatchRealign .. autofunction:: setBatchReslice +.. autofunction:: setBatchResults .. autofunction:: setBatchSaveCoregistrationMatrix .. autofunction:: setBatchSegmentation .. autofunction:: setBatchSelectAnat .. autofunction:: setBatchSkullStripping .. autofunction:: setBatchSmoothConImages +.. autofunction:: setBatchSmoothingFunc .. autofunction:: setBatchSmoothing .. autofunction:: setBatchSTC .. autofunction:: setBatchSubjectLevelContrasts -.. autofunction:: setBatchSubjectLevelGLMSpec - \ No newline at end of file +.. autofunction:: setBatchSubjectLevelGLMSpec +.. autofunction:: setBatchSubjectLevelResults \ No newline at end of file diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst index d9855ed0..de47e77f 100644 --- a/docs/source/contributing.rst +++ b/docs/source/contributing.rst @@ -47,38 +47,6 @@ Template proposal .. autofunction:: templateFunction ----- - -Numpy template --------------- - -See more information `here -`_ - -:: - - function [argout] = templateFunction(argin1, argin2, argin3) - % - % Short description of what the function does goes here. - % - % y = templateFunction(argin1, argin2, argin3) - % - % Parameters: - % argin1: The first input value - % - % argin2: The second input value - % - % argin3: The third input value - % - % Returns: - % The output value - - % The code goes below - - end - -.. autofunction:: templateFunctionNumpy - ---- Google template diff --git a/docs/source/demos.rst b/docs/source/demos.rst new file mode 100644 index 00000000..31b2d9e4 --- /dev/null +++ b/docs/source/demos.rst @@ -0,0 +1,11 @@ +Demos +===== + +:: + + demos/ + ├── face_repetition + ├── lesion_detection + ├── MoAE + ├── openneuro + └── vismotion \ No newline at end of file diff --git a/docs/source/function_description.rst b/docs/source/function_description.rst index 10745525..efa37956 100644 --- a/docs/source/function_description.rst +++ b/docs/source/function_description.rst @@ -16,7 +16,6 @@ List of functions in the ``src`` folder. .. autofunction:: getPrefix .. autofunction:: getRealignParamFile .. autofunction:: getSliceOrder -.. autofunction:: getSpecificSubjects .. autofunction:: setDerivativesDir .. autofunction:: unzipImgAndReturnsFullpathName @@ -25,7 +24,6 @@ Subject level model .. automodule:: src.subject_level -.. autofunction:: concatBetaImgTmaps .. autofunction:: convertOnsetTsvToMat .. autofunction:: createAndReturnOnsetFile .. autofunction:: deleteResidualImages @@ -62,15 +60,26 @@ Utility functions .. automodule:: src.utils .. autofunction:: checkDependencies +.. autofunction:: cleanCrash .. autofunction:: createDataDictionary +.. autofunction:: createDerivativeDir +.. autofunction:: createGlmDirName .. autofunction:: getEnvInfo +.. autofunction:: getSubjectList .. autofunction:: getVersion .. autofunction:: isOctave .. autofunction:: loadAndCheckOptions +.. autofunction:: manageWorkersPool +.. autofunction:: printBatchName .. autofunction:: printCredits .. autofunction:: printProcessingRun .. autofunction:: printProcessingSubject +.. autofunction:: printWorklowName +.. autofunction:: removeSpmPrefix +.. autofunction:: rmTrialTypeStr .. autofunction:: saveMatlabBatch .. autofunction:: saveOptions -.. autofunction:: setDefaultFields +.. autofunction:: setFields +.. autofunction:: setGraphicWindow .. autofunction:: validationInputFile +.. autofunction:: writeDatasetDescription \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index aafb80c3..5c2de617 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -11,10 +11,11 @@ Welcome to CPP SPM BIDS pipeline documentation! :caption: Content set_up - workflows - QA defaults + demos + workflows batches + QA function_description method_section_boilerplate mancoreg @@ -22,7 +23,7 @@ Welcome to CPP SPM BIDS pipeline documentation! contributing This pipeline contains a set of functions to run fMRI analysis on a -[BIDS data set](https://bids.neuroimaging.io/) using SPM12. +`BIDS data set `_ using SPM12. This can perform: diff --git a/docs/source/mancoreg.rst b/docs/source/mancoreg.rst index 5dacf6f9..1460a2a6 100644 --- a/docs/source/mancoreg.rst +++ b/docs/source/mancoreg.rst @@ -5,7 +5,7 @@ Manual coregistration tools ---- -.. automodule:: src.mancoreg +.. automodule:: lib.mancoreg .. autofunction:: mancoreg .. autofunction:: mancoregCallbacks diff --git a/docs/source/workflows.rst b/docs/source/workflows.rst index 06ffadd8..29cfe6d0 100644 --- a/docs/source/workflows.rst +++ b/docs/source/workflows.rst @@ -103,6 +103,13 @@ Compute results =============== .. autofunction:: bidsResults +.. autofunction:: bidsConcatBetaTmaps + +Region of interest analysis +=========================== + +.. autofunction:: bidsRoiBasedGLM +.. autofunction:: bidsCreateROI Other ===== diff --git a/initCppSpm.m b/initCppSpm.m new file mode 100644 index 00000000..0ef52513 --- /dev/null +++ b/initCppSpm.m @@ -0,0 +1,39 @@ +function initCppSpm() + % + % adds the relevant folders to the path + % + % (C) Copyright 2021 CPP_SPM developers + + 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')); + addpath(fullfile(thisDirectory, 'lib', 'slice_display')); + addpath(fullfile(thisDirectory, 'lib', 'panel-2.14')); + addpath(fullfile(thisDirectory, 'lib', 'brain_colours', 'code')); + + checkDependencies(); + + printCredits(); + + run(fullfile(thisDirectory, 'lib', 'CPP_ROI', 'initCppRoi')); + + CPP_SPM_INITIALIZED = true(); + + else + fprintf('\n\nCPP_SPM already initialized\n\n'); + + end + +end diff --git a/lib/CPP_ROI b/lib/CPP_ROI new file mode 160000 index 00000000..a53b52aa --- /dev/null +++ b/lib/CPP_ROI @@ -0,0 +1 @@ +Subproject commit a53b52aa5e971c73b1cf75c2e4a745701d2c28a5 diff --git a/lib/brain_colours b/lib/brain_colours new file mode 160000 index 00000000..68f9a629 --- /dev/null +++ b/lib/brain_colours @@ -0,0 +1 @@ +Subproject commit 68f9a6297ae77365cb550731fe61acb352fa14b7 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 00000000..b5f4010d Binary files /dev/null and b/lib/panel-2.14/docs/demopanelI.png differ diff --git a/lib/panel-2.14/docs/export.png b/lib/panel-2.14/docs/export.png new file mode 100644 index 00000000..994c1fe8 Binary files /dev/null and b/lib/panel-2.14/docs/export.png differ diff --git a/lib/panel-2.14/docs/export_thumb.png b/lib/panel-2.14/docs/export_thumb.png new file mode 100644 index 00000000..e6fdcc3b Binary files /dev/null and b/lib/panel-2.14/docs/export_thumb.png differ diff --git a/lib/panel-2.14/docs/index.png b/lib/panel-2.14/docs/index.png new file mode 100644 index 00000000..57331d2d Binary files /dev/null and b/lib/panel-2.14/docs/index.png differ diff --git a/lib/panel-2.14/docs/index_screenshot.png b/lib/panel-2.14/docs/index_screenshot.png new file mode 100644 index 00000000..e744fc40 Binary files /dev/null and b/lib/panel-2.14/docs/index_screenshot.png differ diff --git a/lib/panel-2.14/docs/index_thumb.png b/lib/panel-2.14/docs/index_thumb.png new file mode 100644 index 00000000..6abc012e Binary files /dev/null and b/lib/panel-2.14/docs/index_thumb.png differ diff --git a/lib/panel-2.14/docs/layout.png b/lib/panel-2.14/docs/layout.png new file mode 100644 index 00000000..3ce4a6c3 Binary files /dev/null and b/lib/panel-2.14/docs/layout.png differ diff --git a/lib/panel-2.14/docs/layout_thumb.png b/lib/panel-2.14/docs/layout_thumb.png new file mode 100644 index 00000000..568e7003 Binary files /dev/null and b/lib/panel-2.14/docs/layout_thumb.png differ diff --git a/lib/panel-2.14/docs/panel.css b/lib/panel-2.14/docs/panel.css new file mode 100644 index 00000000..f43c6a4f --- /dev/null +++ b/lib/panel-2.14/docs/panel.css @@ -0,0 +1,63 @@ + +body +{ + font-family:segoe ui, verdana; + font-size:medium; + color:#335; + max-width:1000px; + margin:auto; + background-color:gray; +} + +#main +{ + background-color:white; + padding:32px; +} + +.note +{ + background-color:#d96; + color:white; + padding:16px; + font-size:small; +} + +h1, h2, h3, h4, h5 +{ + font-family:Segoe UI, Times; + color:black; +} + +a:link, a:visited +{ + color:blue; + text-decoration:none; +} + +a:hover +{ + text-decoration:underline; +} + +tt, pre, dh.ref, .snippet +{ + font-family:"andale mono", "courier new"; + color:#930; + font-weight:bold; +} + +dh +{ + font-weight:bold; +} + +dd +{ + margin-bottom:24px; +} + +.snippet +{ + white-space:pre; +} diff --git a/lib/panel-2.14/license.txt b/lib/panel-2.14/license.txt new file mode 100644 index 00000000..199dcd43 --- /dev/null +++ b/lib/panel-2.14/license.txt @@ -0,0 +1,25 @@ +Copyright (c) 2019, Ben Mitch +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution +* Neither the name of nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/panel-2.14/panel.m b/lib/panel-2.14/panel.m new file mode 100644 index 00000000..92b0c070 --- /dev/null +++ b/lib/panel-2.14/panel.m @@ -0,0 +1,5201 @@ + +% Panel is an alternative to Matlab's "subplot" function. +% +% INSTALLATION. To install panel, place the file "panel.m" +% on your Matlab path. +% +% DOCUMENTATION. Scan the introductory information in the +% folder "docs". Learn to use panel by working through the +% demonstration scripts in the folder "demo" (list the demos +% by typing "help panel/demo"). Reference information is +% available through "doc panel" or "help panel". For the +% change log, use "edit panel" to view the file "panel.m". + + + +% CHANGE LOG +% +% ############################################################ +% 22/05/2011 +% First Public Release Version 2.0 +% ############################################################ +% +% 23/05/2011 +% Incorporated an LP solver, since the one we were using +% "linprog()" is not available to users who do not have the +% Optimisation Toolbox installed. +% +% 21/06/2011 +% Added -opdf option, and changed PageSize to be equal to +% PaperPosition. +% +% 12/07/2011 +% Made some linprog optimisations, inspired by "Ian" on +% Matlab central. Tested against subplot using +% demopanel2(N=9). Subplot is faster, by about 20%, but +% panel is better :). For my money, 20% isn't much of a hit +% for the extra functionality. NB: Using Jeff Stuart's +% linprog (unoptimised), panel is much slower (especially +% for large N problems); we will probably have to offer a +% faster solver at some point (optimise Jeff's?). +% +% NOTES: You will see a noticeable delay, also, on resize. +% That's the price of using physical units for the layout, +% because we have to recalculate everything when the +% physical canvas size changes. I suppose in the future, we +% could offer an option so that physical units are only used +% during export; that would make resizes fast, and the user +% may not care so much about layout on screen, if they are +% aiming for print figures. Or, you could have the ability +% to turn off auto-refresh on resize(). +% +% ############################################################ +% 20/07/2011 +% Release Version 2.1 +% ############################################################ +% +% 05/10/2011 +% Tidied in-file documentation (panel.m). +% +% 11/12/2011 +% Added flag "no-manage-font" to constructor, as requested +% by Matlab Central user Mukhtar Ullah. +% +% ############################################################ +% 13/12/2011 +% Release Version 2.2 +% ############################################################ +% +% 21/01/2012 +% Fixed bug in explicit height export option "-hX" which +% wasn't working right at all. +% +% 25/01/12 +% Fixed bug in tick label display during print. _Think_ I've +% got it right, this time! Some notes below, search for +% "25/01/12". +% +% 25/01/12 +% Fixed DPI bug in smoothed export figures. Bug was flagged +% up by Jesper at Matlab Central. +% +% ############################################################ +% 26/01/2012 +% Release Version 2.3 +% ############################################################ +% +% 09/03/12 +% Fixed bug whereby re-positioning never got done if only +% one panel was created in an existing figure window. +% +% ############################################################ +% 13/03/2012 +% Release Version 2.4 +% ############################################################ +% +% 15/03/12 +% NB: On 2008b, and possibly later versions, the fact that +% the resizeCallback() and closeCallback() are private makes +% things not work. You can fix this by removing the "Access +% = Private" modifier on that section of "methods". It works +% fine in later versions, they must have changed the access +% rules I guess. +% +% 19/07/12 +% Modified so that more than one object can be managed by +% one axis. Just use p.select([h1 h2 ...]). Added function +% "getAllManagedAxes()" which returns only objects from the +% "object list" (h_object), as it now is, which represent +% axes. Suggested by Brendan Sullivan @ Matlab Central. +% +% 19/07/12 +% Added support for zlabel() call (not applicable to parent +% panels, since they are implicitly 2D for axis labelling). +% +% 19/07/12 +% Fixed another export bug - how did this one not get +% noticed? XLimMode (etc.) was not getting locked during +% export, so that axes without manual limits might get +% re-dimensioned during export, which is bad news. Added +% locking of limits as well as ticks, in storeAxisState(). +% Hope this has no side effects! +% +% ############################################################ +% 19/07/12 +% Release Version 2.5 +% +% NB: Owing to the introduction of management of multiple +% objects by each panel, this release should be considered +% possibly flaky. Revert to 2.4 if you have problems with +% 2.5. +% ############################################################ +% +% 23/07/12 +% Improved documentation for figure export in demopanelA. +% +% 24/07/12 +% Added support for export to SVG, using "plot2svg" (Matlab +% Central File Exchange) as the renderer. Along the way, +% tidied the behaviour of export() a little, and improved +% reporting to the user. Changed default DPI for EPS to 600, +% since otherwise the output files are pretty shoddy, and +% the filesize is relatively unaffected. +% +% 24/07/12 +% Updated documentation, particularly HTML pages and +% associated figures. Bit nicer, now. +% +% ############################################################ +% 24/07/12 +% Release Version 2.6 +% ############################################################ +% +% 22/09/12 +% Added demopanelH, which illustrates how to do insets. Kudos +% to Ann Hickox for the idea. +% +% 20/03/13 +% Added panel.plot() to work around poor rendering of dashed +% lines, etc. Added demopanelI to illustrate its use. +% +% 20/03/13 +% Renamed setCallback to addCallback, so we can have more +% than one. Added "userdata" argument to addCallback(), and +% "event" field (and "userdata" field) to "data" passed when +% callback is fired. +% +% ############################################################ +% 21/03/13 +% Release Version 2.7 +% ############################################################ +% +% 21/03/13 +% Fixed bug in panel.plot() which did not handle solid lines +% correctly. +% +% 12/04/13 +% Added back setCallback() with appropriate semantics, for +% the use of legacy code (or, really, future code, these +% semantics might be useful to someone). Also added the +% function clearCallbacks(). +% +% 12/04/13 +% Removed panel.plot() because it just seemed to be too hard +% to manage. Instead, we'll let the user plot things in the +% usual way, but during export (when things are well under +% our control), we'll fix up any dashed lines that the user +% has requested using the call fixdash(). Thus, we apply the +% fix only where it's needed, when printing to an image +% file, and save all the faffing with resize callbacks. +% +% ############################################################ +% 12/04/13 +% Release Version 2.8 +% ############################################################ +% +% 13/04/13 +% Changed panel.export() to infer image format from file +% extension, in the case that it is not explicitly specified +% and the passed filename has an extension. +% +% 03/05/13 +% Changed term "render", where misused, to "layout", so as +% not to confuse users of the help/docs. Changed name of +% callback event from "render-complete" to "layout-updated", +% is the only functional effect. +% +% 03/05/13 +% Added argument to panel constructor so that units can be +% set there, rather than through a separate call to the +% "units" property. +% +% 03/05/13 +% Added set descriptor "family" to go with "children" and +% "descendants". This one should be of particular use for +% the construct p.fa.margin = 0. +% +% 03/05/13 +% Updated demopanel9 to be a walkthrough of how to set +% margins. Will be useful to point users at this if they ask +% "how do I do margins?". +% +% 03/05/13 +% Added panel.version(). +% +% 03/05/13 +% Added page size "LNCS" to export. +% +% ############################################################ +% 03/05/13 +% Release Version 2.9 +% ############################################################ +% +% 10/05/13 +% Removed linprog solution in favour of recursive +% computation. This should speed things up for people who +% don't have the optimisation toolbox. +% +% 10/05/13 +% Added support for panels of fixed physical size. See new +% documentation for panel/pack(). +% +% 10/05/13 +% Added support for packing into panels packed in absolute +% mode, which wasn't previously possible. +% +% 10/05/13 +% Removed advertisement for 'defer' flag, since I suspect +% it's no longer needed now we've moved away from LP. There +% may be some optimisation required before this is true - +% defer still functions as before, it's just not advertised. +% +% ############################################################ +% 10/05/13 +% Release Version 2.10 +% ############################################################ +% +% 14/05/13 +% Some minor optimisations, so now panel is not slower than +% subplot (see demopanelK). +% +% 14/01/15 +% Various fixes to work correctly under R2014b. Essentially, +% checked the demos, added retirement notes to fixdash(), and +% added function "fignum()". +% +% ############################################################ +% 14/01/15 +% Release Version 2.11 +% ############################################################ +% +% 28/03/15 +% Changed export() logic slightly so that if either -h or -w option is +% specified, direct sizing model is selected (and, therefore, /all/ +% options from the paper sizing model are ignored). Thus, either -w or +% -h can be specified, with -a, and intuitively-correct behaviour +% results. +% +% 02/04/15 +% Changed functions x/y/zlabel and title to return a handle to the +% referenced object so that caller can access its properties. +% +% ############################################################ +% 02/04/15 +% Release Version 2.12 +% ############################################################ +% +% 30/07/19 +% Fixed bug in dereferencing 'children' field, not sure when +% this was introduced but behaviour is now correct. +% +% ############################################################ +% 30/07/19 +% Release Version 2.13 +% ############################################################ +% +% 02/08/19 +% Fixed display bug. +% +% 02/08/19 +% Added find() method. +% +% 02/08/19 +% Removed rejection of re-select()-ing managed objects of a +% panel, because it seems an unnecessary restriction. +% +% 21/11/19 +% Changed uistack position of axes that are present only to +% position labels to 'bottom', allowing mouse interactions +% with the underlying axes (thanks to File Exchange user +% 'zwbxyzeng' for the heads-up). +% +% ############################################################ +% 21/11/19 +% Release Version 2.14 +% ############################################################ + +classdef (Sealed = true) panel < handle + + + + %% ---- PROPERTIES ---- + + properties (Constant = true, Hidden = true) + + PANEL_TYPE_UNCOMMITTED = 0; + PANEL_TYPE_PARENT = 1; + PANEL_TYPE_OBJECT = 2; + + end + + properties (Constant = true) + + LAYOUT_MODE_NORMAL = 0; + LAYOUT_MODE_PREPRINT = 1; + LAYOUT_MODE_POSTPRINT = 2; + + end + + properties + + % these properties are only here for documentation. they + % are actually stored in "prop". it's just some subsref + % madness. + + % font name to use for axis text (inherited) + % + % access: read/write + % default: normal + fontname + + % font size to use for axis text (inherited) + % + % access: read/write + % default: normal + fontsize + + % font weight to use for axis text (inherited) + % + % access: read/write + % default: normal + fontweight + + % the units that are used when reading/writing margins + % + % units can be set to any of 'mm', 'cm', 'in' or 'pt'. + % it only affects the read/write interface; values + % stored already are not re-interpreted. + % + % access: read/write + % default: mm + units + + % the panel's margin vector in the form [left bottom right top] + % + % the margin is key to the layout process. the layout + % algorithm makes all panels as large as possible whilst + % not violating margin constraints. margins are + % respected between panels within their parent and + % between the root panel and the edges of the canvas + % (figure or image file). + % + % access: read/write + % default: [12 10 2 2] (mm) + % + % see also: marginleft, marginbottom, marginright, margintop + margin + + % one element of the margin vector + % + % access: read/write + % default: see margin + % + % see also: margin + marginleft + + % one element of the margin vector + % + % access: read/write + % default: see margin + % + % see also: margin + marginbottom + + % one element of the margin vector + % + % access: read/write + % default: see margin + % + % see also: margin + marginright + + % one element of the margin vector + % + % access: read/write + % default: see margin + % + % see also: margin + margintop + + % return position of panel + % + % return the panel's position in normalized + % coordinates (normalized to the figure window that + % is associated with the panel). note that parent + % panels have positions too, even though nothing is + % usually rendered. uncommitted panels, too. + % + % access: read only + position + + % return handle of associated figure + % + % access: read only + figure + + % return handle of associated axis + % + % if the panel is not an axis panel, empty is returned. + % object includes axis, but axis does not include + % object. + % + % access: read only + % + % see also: object + axis + + % return handle of associated object + % + % if the panel is not an object panel, empty is + % returned. object includes axis, but axis does not + % include object. + % + % access: read only + % + % see also: axis + object + + % access properties of panel's children + % + % if the panel is a parent panel, "children" gives + % access to some properties of its children (direct + % descendants). "children" can be abbreviated "ch". + % properties that can be accessed are as follows. + % + % axis: read-only, returns an array + % object: read-only, returns an array + % + % margin: write-only + % fontname: write-only + % fontsize: write-only + % fontweight: write-only + % + % EXAMPLE: + % h = p.ch.axis; + % p.ch.margin = 3; + % + % see also: descendants, family + children + + % access properties of panel's descendants + % + % if the panel is a parent panel, "descendants" gives + % access to some properties of its descendants + % (children, grandchildren, etc.). "descendants" can be + % abbreviated "de". properties that can be accessed are + % as follows. + % + % axis: read-only, returns an array + % object: read-only, returns an array + % + % margin: write-only + % fontname: write-only + % fontsize: write-only + % fontweight: write-only + % + % EXAMPLE: + % h = p.de.axis; + % p.de.margin = 3; + % + % see also: children, family + descendants + + % access properties of panel's family + % + % if the panel is a parent panel, "family" gives access + % to some properties of its family (self, children, + % grandchildren, etc.). "family" can be abbreviated + % "fa". properties that can be accessed are as follows. + % + % axis: read-only, returns an array + % object: read-only, returns an array + % + % margin: write-only + % fontname: write-only + % fontsize: write-only + % fontweight: write-only + % + % EXAMPLE: + % h = p.fa.axis; + % p.fa.margin = 3; + % + % see also: children, descendants + family + + end + + properties (Access = private) + + % associated figure window + h_figure + + % parent graphics object + h_parent + + % this is empty for the root PANEL, populated for all others + parent + + % this is always the root panel associated with this + m_root + + % packing specifier + % + % empty: relative positioning mode (stretch) + % scalar fraction: relative positioning mode + % scalar percentage: relative positioning mode + % 1x4 dimension: absolute positioning mode + packspec + + % packing dimension of children + % + % 1 : horizontal + % 2 : vertical + packdim + + % panel type + m_panelType + + % fixdash lines + m_fixdash + m_fixdash_restore + + % associated managed graphics object (usually, an axis) + h_object + + % show axis (only the root has this extra axis, if show() is active) + h_showAxis + + % children (only a parent panel has non-empty, here) + m_children + + % callback (any functions listed in this cell array are called when events occur) + m_callback + + % local properties (actual properties is this overlaid on inherited/default properties) + % + % see getPropertyValue() + prop + + % state + % + % private state information used during various processing + state + + % layout context for this panel + % + % this is the layout context for the panel. this is + % computed in the function recomputeLayout(), and used + % to reposition the panel in applyLayout(). storage of + % this data means that we can call applyLayout() to + % layout only a branch of the panel tree without having + % to recompute the whole thing. however, I don't know + % how efficient this system is, might need some work. + m_context + + end + + + + + + + + %% ---- PUBLIC CTOR/DTOR ---- + + methods + + function p = panel(varargin) + + % create a new panel + % + % p = panel(...) + % create a new root panel. optional arguments listed + % below can be supplied in any order. if "h_parent" + % is not supplied, it is set to gcf - that is, the + % panel fills the current figure. + % + % initially, the root panel is an "uncommitted + % panel". calling pack() or select() on it will + % commit it as a "parent panel" or an "object + % panel", respectively. the following arguments may + % be passed, in any order. + % + % h_parent + % a handle to a graphics object that will act as the + % parent of the new panel. this is usually a figure + % handle, but may be a handle to any graphics + % object, in principle. currently, an error is + % raised unless it's a figure or a uipanel - if you + % want to try other object types, edit the code + % where the error is raised, and let me know if you + % have positive results so I can update panel to + % allow other object types. + % + % 'add' + % usually, when you attach a new root panel to a + % figure, any existing attached root panels are + % first deleted to make way for it. if you pass this + % argument, this is not done, so that you can attach + % more than one root panel to the same figure. see + % demopanelE for an example of this use. + % + % 'no-manage-font' + % by default, a panel will manage fonts of titles + % and axis labels. this prevents the user from + % setting individual fonts on those items. pass this + % flag to disable font management for this panel. + % + % 'mm', 'cm', 'in', 'pt' + % by default, panel uses mm as the unit of + % communication with the user over margin sizes. + % pass any of these to change this (you can achieve + % the same effect after creating a panel by setting + % the property "units"). + % + % see also: panel (overview), pack(), select() + + % PRIVATE DOCUMENTATION + % + % 'defer' + % THIS IS NO LONGER ADVERTISED since we replaced the + % LP solution with a procedural solution, but still + % functions as before, to provide legacy support. + % the panel will be created with layout disabled. + % the layout computations take a little while when + % large numbers of panels are involved, and are + % re-run every time you add a panel or change a + % margin, by default. this is tedious if you are + % preparing a complex layout; pass 'defer', and + % layout will not be computed at all until you call + % refresh() or export() on the root panel. + % + % 'pack' + % this constructor is called internally from pack() + % to create new panels when packing them into + % parents. the first argument is passed as 'pack' in + % this case, which allows us to do slightly quicker + % parsing of the arguments, since we know the + % calling convention (see immediately below). + + % default state + p.state = []; + p.state.name = ''; + p.state.defer = 0; + p.state.manage_font = 1; + p.m_callback = {}; + p.m_fixdash = {}; + p.packspec = []; + p.packdim = 2; + p.m_panelType = p.PANEL_TYPE_UNCOMMITTED; + p.prop = panel.getPropertyInitialState(); + + % handle call from pack() aqap + if nargin && isequal(varargin{1}, 'pack') + + % since we know the calling convention, in this + % case, we can handle this as quickly as possible, + % so that large recursive layouts do not get held up + % by spurious code, here. + + % parent is a panel + passed_h_parent = varargin{2}; + + % become its child + indexInParent = int2str(length(passed_h_parent.m_children)+1); + if passed_h_parent.isRoot() + p.state.name = ['(' indexInParent ')']; + else + p.state.name = [passed_h_parent.state.name(1:end-1) ',' indexInParent ')']; + end + p.h_parent = passed_h_parent.h_parent; + p.h_figure = passed_h_parent.h_figure; + p.parent = passed_h_parent; + p.m_root = passed_h_parent.m_root; + + % done! + return + + end + + % default condition + passed_h_parent = []; + add = false; + + % peel off args + while ~isempty(varargin) + + % get arg + arg = varargin{1}; + varargin = varargin(2:end); + + % handle text + if ischar(arg) + + switch arg + + case 'add' + add = true; + continue; + + case 'defer' + p.state.defer = 1; + continue; + + case 'no-manage-font' + p.state.manage_font = 0; + continue; + + case {'mm' 'cm' 'in' 'pt'} + p.setPropertyValue('units', arg); + continue; + + otherwise + error('panel:InvalidArgument', ['unrecognised text argument "' arg '"']); + + end + + end + + % handle parent + if isscalar(arg) && ishandle(arg) + passed_h_parent = arg; + continue; + end + + % error + error('panel:InvalidArgument', 'unrecognised argument to panel constructor'); + + end + + % attach to current figure if no parent supplied + if isempty(passed_h_parent) + passed_h_parent = gcf; + + % this might cause a figure to be created - if so, + % give it time to display now so we don't get a (or + % two, in fact!) resize event(s) later + drawnow + end + + % we are a root panel + p.state.name = 'root'; + p.parent = []; + p.m_root = p; + + % get parent type + parentType = get(passed_h_parent, 'type'); + + % set handles + switch parentType + + case 'uipanel' + p.h_parent = passed_h_parent; + p.h_figure = getParentFigure(passed_h_parent); + + case 'figure' + p.h_parent = passed_h_parent; + p.h_figure = passed_h_parent; + + otherwise + error('panel:InvalidArgument', ... + ['panel() cannot be attached to an object of type "' parentType '"']); + + end + + % lay in callbacks + addHandleCallback(p.h_figure, 'CloseRequestFcn', @panel.closeCallback); + addHandleCallback(p.h_parent, 'ResizeFcn', @panel.resizeCallback); + + % register for callbacks + if add + panel.callbackDispatcher('registerNoClear', p); + else + panel.callbackDispatcher('register', p); + end + + % lock class in memory (prevent persistent from being cleared by clear all) + panel.lockClass(); + + end + + function delete(p) + + % destroy a panel + % + % delete(p) + % destroy the passed panel, deleting all associated + % graphics objects. + % + % NB: you won't usually have to call this explicitly. + % it is called automatically for all attached panels + % when you close the associated figure. + + % debug output +% panel.debugmsg(['deleting "' p.state.name '"...']); + + % delete managed graphics objects + for n = 1:length(p.h_object) + h = p.h_object(n); + if ishandle(h) + delete(h); + end + end + + % delete associated show axis + if ~isempty(p.h_showAxis) && ishandle(p.h_showAxis) + delete(p.h_showAxis); + end + + % delete all children (child will remove itself from + % "m_children" on delete()) + while ~isempty(p.m_children) + delete(p.m_children(end)); + end + + % unregister... + if p.isRoot() + + % ...for callbacks + panel.callbackDispatcher('unregister', p); + + else + + % ...from parent + p.parent.removeChild(p); + + end + + % debug output +% panel.debugmsg(['deleted "' p.state.name '"!']); + + end + + end + + + + + + + + + + + + + + + %% ---- PUBLIC DISPLAY ---- + + methods (Hidden = true) + + function disp(p) + + display(p); + + end + + function display(p, label) + + if nargin == 2 + disp([10 label ' =' 10]) + end + + display_sub(p); + + disp(' ') + + end + + function display_sub(p, indent) + + % default + if nargin < 2 + indent = ''; + end + + % handle non-scalar (should not exist!) + nels = numel(p); + if nels > 1 + sz = size(p); + sz = sprintf('%dx', sz); + disp([sz(1:end-1) ' array of panel objects']); + return + end + + % header + header = indent; + if p.isObject() + header = [header 'Object ' p.state.name ': ']; + elseif p.isParent() + header = [header 'Parent ' p.state.name ': ']; + else + header = [header 'Uncommitted ' p.state.name ': ']; + end + if p.isRoot() + pp = ['attached to Figure ' panel.fignum(p.h_figure)]; + else + if isempty(p.packspec) + pp = 'stretch'; + elseif iscell(p.packspec) + units = p.getPropertyValue('units'); + val = panel.resolveUnits({p.packspec{1} 'mm'}, units); + pp = sprintf('%.1f%s', val, units); + elseif isscalar(p.packspec) + if p.packspec > 1 + pp = sprintf('%.0f%%', p.packspec); + else + pp = sprintf('%.3f', p.packspec); + end + else + pp = sprintf('%.3f ', p.packspec); + pp = pp(1:end-1); + end + end + header = [header '[' pp]; + if p.isParent() + edges = {'hori' 'vert'}; + header = [header ', ' edges{p.packdim}]; + end + header = [header ']']; + + % margin + header = rpad(header, 60); + header = [header '[ margin ' sprintf('%.3g ', p.getPropertyValue('margin')) p.getPropertyValue('units') ']']; + +% % index +% if isfield(p.state, 'index') +% header = [header ' (' int2str(p.state.index) ')']; +% end + + % display + disp(header); + + % children + for c = 1:length(p.m_children) + p.m_children(c).display_sub([indent ' ']); + end + + end + + end + + + + + + + + + + + %% ---- PUBLIC METHODS ---- + + methods + + function h = xlabel(p, text) + + % apply an xlabel to the panel (or group) + % + % p.xlabel(...) + % behaves just like xlabel() at the prompt (you can + % use that as an alternative) when called on an axis + % panel. when called on a parent panel, however, the + % group of objects within that parent have a label + % applied. when called on a non-axis object panel, + % an error is raised. + + h = get(p.getOrCreateAxis(), 'xlabel'); + set(h, 'string', text); + if p.isParent() + set(h, 'visible', 'on'); + end + + end + + function h = ylabel(p, text) + + % apply a ylabel to the panel (or group) + % + % p.ylabel(...) + % behaves just like ylabel() at the prompt (you can + % use that as an alternative) when called on an axis + % panel. when called on a parent panel, however, the + % group of objects within that parent have a label + % applied. when called on a non-axis object panel, + % an error is raised. + + h = get(p.getOrCreateAxis(), 'ylabel'); + set(h, 'string', text); + if p.isParent() + set(h, 'visible', 'on'); + end + + end + + function h = zlabel(p, text) + + % apply a zlabel to the panel (or group) + % + % p.zlabel(...) + % behaves just like zlabel() at the prompt (you can + % use that as an alternative) when called on an axis + % panel. when called on a parent panel, however, + % this method raises an error, since parent panels + % are assumed to be 2D, with respect to axes. + + if p.isParent() + error('panel:ZLabelOnParentAxis', 'can only call zlabel() on an object panel'); + end + + h = get(p.getOrCreateAxis(), 'zlabel'); + set(h, 'string', text); + + end + + function h = title(p, text) + + % apply a title to the panel (or group) + % + % p.title(...) + % behaves just like title() at the prompt (you can + % use that as an alternative) when called on an axis + % panel. when called on a parent panel, however, the + % group of objects within that parent have a title + % applied. when called on a non-axis object panel, + % an error is raised. + + h = title(p.getOrCreateAxis(), text); + if p.isParent() + set(h, 'visible', 'on'); + end + + end + + function hold(p, state) + + % set the hold on/off state of the associated axis + % + % p.hold('on' / 'off') + % you can use matlab's "hold" function with plots in + % panel, just like any other plot. there is, + % however, a very minor gotcha that is somewhat + % unlikely to ever come up, but for completeness + % this is the problem and the solutions: + % + % if you create a panel "p", change its font using + % panel, e.g. "p.fontname = 'Courier New'", then call + % "hold on", then "hold off", then plot into it, the + % font is not respected. this situation is unlikely to + % arise because there's usually no reason to call + % "hold off" on a plot. however, there are three + % solutions to get round it, if it does: + % + % a) call p.refresh() when you're finished, to + % update all fonts to managed values. + % + % b) if you're going to call p.export() anyway, + % fonts will get updated when you do. + % + % c) if for some reason you can't do (a) OR (b) (I + % can't think why), you can use the hold() function + % provided by panel instead of that provided by + % Matlab. this will not affect your fonts. for + % example, call "p(2).hold('on')". + + % because the matlab "hold off" command sets an axis's + % nextplot state to "replace", we lose control over + % the axis properties (such as fontname). we set + % nextplot to "replacechildren" when we create an + % axis, but if the user does a "hold on, hold off" + % cycle, we lose that. therefore, we offer this + % alternative. + + % check + if ~p.isObject() + error('panel:HoldWhenNotObjectPanel', 'can only call hold() on an object panel'); + end + + % check + h_axes = p.getAllManagedAxes(); + if isempty(h_axes) + error('panel:HoldWhenNoAxes', 'can only call hold() on a panel that manages one or more axes'); + end + + % switch + switch state + case {'on' true 1} + set(h_axes, 'nextplot', 'add'); + case {'off' false 0} + set(h_axes, 'nextplot', 'replacechildren'); + otherwise + error('panel:InvalidArgument', 'argument to hold() must be ''on'', ''off'', or boolean'); + end + + end + + function fixdash(p, hs, linestyle) + + % pass dashed lines to be fixed up during 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. + % + % p.fixdash(h, linestyle) + % add the lines specified as handles in "h" to the + % list of lines to be "fixed up" during export. + % panel will attempt to get the lines to look right + % during export to all formats where they would + % usually get mussed up. see demopanelI for an + % example of how it works. + % + % the above is the usual usage of fixdash(), but + % you can get more control over linestyle by + % specifying the additional argument, "linestyle". + % if "linestyle" is supplied, it is used as the + % linestyle; if not, the current linestyle of the + % line (-, --, -., :) is used. "linestyle" can + % either be a text string or a series of numbers, as + % described below. + % + % '-' solid + % '--' dashed, equal to [2 0.75] + % '-.' dash-dot, equal to [2 0.75 0.5 0.75] + % ':', '.' dotted, equal to [0.5 0.5] + % + % a number series should be 1xN, where N is a + % multiple of 2, as in the examples above, and + % specifies the lengths of any number of dash + % components that are used before being repeated. + % for instance, '-.' generates a 2 unit segment + % (dash), a 0.75 unit gap, then a 0.5 unit segment + % (dot) and a final 0.75 unit gap. at present, the + % units are always millimetres. this system is + % extensible, so that the following examples are + % also valid: + % + % '--..' dash-dash-dot-dot + % '-..-.' dash-dot-dot-dash-dot + % [2 1 4 1 6 1] 2 dash, 4 dash, 6 dash + + % default + if nargin < 3 + linestyle = []; + end + + % bubble up to root + if ~p.isRoot() + p.m_root.fixdash(hs, linestyle); + return + end + + % for each passed handle + for h = (hs(:)') + + % check it's still a handle + if ~ishandle(h) + continue + end + + % check it's a line + if ~isequal(get(h, 'type'), 'line') + continue + end + + % update if in list + found = false; + for i = 1:length(p.m_fixdash) + if h == p.m_fixdash{i}.h + p.m_fixdash{i}.linestyle = linestyle; + found = true; + break + end + end + + % else add to list + if ~found + p.m_fixdash{end+1} = struct('h', h, 'linestyle', linestyle); + end + + end + + end + + function show(p) + + % highlight the outline of the panel + % + % p.show() + % make the outline of the panel "p" show up in red + % in the figure window. this is useful for + % understanding a complex layout. + % + % see also: identify() + + r = p.getObjectPosition(); + + if ~isempty(r) + h = p.getShowAxis(); + delete(get(h, 'children')); + xdata = [r(1) r(1)+r(3) r(1)+r(3) r(1) r(1)]; + ydata = [r(2) r(2) r(2)+r(4) r(2)+r(4) r(2)]; + plot(h, xdata, ydata, 'r-', 'linewidth', 5); + axis([0 1 0 1]) + end + + end + + function export(p, varargin) + + % to export the root panel to an image file + % + % p.export(filename, ...) + % + % export the figure containing panel "p" to an image file. + % you must supply the filename of this output file, with or + % without a file extension. any further arguments must be + % option strings starting with the dash ("-") character. "p" + % should be the root panel. + % + % if the filename does not include an extension, the + % appropriate extension will be added. if it does, the + % output format will be inferred from it, unless overridden + % by the "-o" option, described below. + % + % if you are targeting a print publication, you may find it + % easiest to size your output using the "paper sizing model" + % (below). if you prefer, you can use the "direct sizing + % model", instead. these two sizing models are described + % below. underneath these are listed the options unrelated + % to sizing (which apply regardless of which sizing model + % you use). + % + % + % + % PAPER SIZING MODEL: + % + % using the paper sizing model, you specify your target as a + % region of a piece of paper, and the actual size in + % millimeters is calculated for you. this is usually very + % convenient, but if you find it unsuitable, the direct + % sizing model (next section) is provided as an alternative. + % + % to specify the region, you specify the type (size) of + % paper, the orientation, the number of columns, and the + % aspect ratio of the figure (or the fraction of a column to + % fill). usually, the remaining options can be left as + % defaults. + % + % -pX + % X is the paper type, A2-A6 or letter (default is A4). + % NB: you can also specify paper type LNCS (Lecture Notes + % in Computer Science), using "-pLNCS". If you do this, + % the margins are also adjusted to match LNCS format. + % + % -l + % specify landscape mode (default is portrait). + % + % -mX + % X is the paper margins in mm. you can provide a scalar + % (same margins all round) or a comma-separated list of + % four values, specifying the left, bottom, right, top + % margins separately (default is 20mm all round). + % + % -iX + % X is the inter-column space in mm (default is + % 5mm). + % + % -cX + % X is the number of columns (default is 1). + % + % NB: the following two options represent two ways to + % specify the height of the figure relative to the space + % defined by the above options. if you supply both, + % whichever comes second will be used. + % + % -aX + % X is the aspect ratio of the resulting image file (width + % is set by the paper model). X can be one of the strings: + % s (square), g (landscape golden ratio), gp (portrait + % golden ratio), h (half-height), d (double-height); or, a + % number greater than zero, to specify the aspect ratio + % explicitly. note that, if using the numeric form, the + % ratio is expressed as the quotient of width over height, + % in the usual way. ratios greater than 10 or less than + % 0.1 are disallowed, since these can cause a very large + % figure file to be created accidentally. default is to + % use the landscape golden ratio. + % + % -fX + % X is the fraction of the column (or page, if there are + % not columns) to fill. X can be one of the following + % strings - a (all), tt (two thirds), h (half), t (third), + % q (quarter) - or a fraction between 0 and 1, to specify + % the fraction of the space to fill as a number. default + % is to use aspect ratio, not fill fraction. + % + % + % + % DIRECT SIZING MODEL: + % + % if one of these two options is set, the output image is + % sized according to that option and the aspect ratio (see + % above) and the paper model is not used. if both are set, + % the aspect ratio is not used either. + % + % -wX + % X is the explicit width in mm. + % + % -hX + % X is the explicit height in mm. + % + % + % + % NON-SIZING OPTIONS: + % + % finally, a few options are provided to control how + % the prepared figure is exported. note that DPI below + % 150 is only recommended for sizing drafts, since + % font and line sizes are not rendered even vaguely + % accurately in some cases. at the other end, DPI + % above 600 is unlikely to be useful except when + % submitting camera-ready copy. + % + % -rX + % X is the resolution (DPI) at which to produce the + % output file. X can be one of the following strings + % - d (draft, 75DPI), n (normal, 150DPI), h (high, + % 300DPI), p (publication quality, 600DPI), x + % (extremely high quality, 1200DPI) - or just + % the DPI as a number (must be in 75-2400). the + % default depends on the output format (see below). + % + % -rX/S + % X is the DPI, S is the smoothing factor, which can + % be 2 or 4. the output file is produced at S times + % the specified DPI, and then reduced in size to the + % specified DPI by averaging. thus, the hard edges + % produced by the renderer are smoothed - the effect + % is somewhat like "anti-aliasing". + % + % NB: the DPI setting might be expected to have no + % effect on vector formats. this is true for SVG, but + % not for EPS, where the DPI affects the numerical + % precision used as well as the size of some image + % elements, but has little effect on file size. for + % this reason, the default DPI is 150 for bitmap + % formats but 600 for vector formats. + % + % -s + % print sideways (default is to print upright) + % + % -oX + % X is the output format - choose from most of the + % built-in image device drivers supported by "print" + % (try "help print"). this includes "png", "jpg", + % "tif", "eps" and "pdf". note that "eps"/"ps" + % resolve to "epsc2"/"psc2", for convenience. to use + % the "eps"/"ps" devices, use "-oeps!"/"-ops!". you + % may also specify "svg", if you have the tool + % "plot2svg" on your path (available at Matlab + % Central File Exchange). the default output format + % is inferred from the file extension, or "png" if + % the passed filename has no extension. + % + % + % + % EXAMPLES: + % + % default export of 'myfig', creates 'myfig.png' at a + % size of 170x105mm (1004x620px). this size comes + % from: A4 (210mm wide), minus two 20mm margins + % (170mm), and using the golden aspect ratio to give a + % height of 105mm, and finally 150DPI to give the + % pixel size. + % + % p.export('myfig') + % + % when producing the final camera-ready image for a + % square figure that will sit in one of the two + % columns of a letter-size paper journal with default + % margins and inter-column space, we might use this: + % + % p.export('myfig', '-pletter', '-c2', '-as', '-rp'); + + % LEGACY + % + % (this is legacy since the 'defer' flag is no longer + % needed - though it is still supported) + % + % NB: if you pass 'defer' to the constructor, calling + % export() both exports the panel and releases the + % defer mode. future changes to properties (e.g. + % margins) will cause immediate recomputation of the + % layout. + + % check + if ~p.isRoot() + error('panel:ExportWhenNotRoot', 'cannot export() this panel - it is not the root panel'); + end + + % used below + default_margin = 20; + + % parameters + pars = []; + pars.filename = ''; + pars.fmt = ''; + pars.ext = ''; + pars.dpi = []; + pars.smooth = 1; + pars.paper = 'A4'; + pars.landscape = false; + pars.fill = -1.618; + pars.cols = 1; + pars.intercolumnspacing = 5; + pars.margin = default_margin; + pars.sideways = false; + pars.width = 0; + pars.height = 0; + invalid = false; + + % interpret args + for a = 1:length(varargin) + + % extract + arg = varargin{a}; + + % all arguments must be non-empty strings + if ~isstring(arg) + error('panel:InvalidArgument', ... + 'all arguments to export() must be non-empty strings'); + end + + % is filename? + if arg(1) ~= '-' + + % error if already set + if ~isempty(pars.filename) + error('panel:InvalidArgument', ... + ['at argument "' arg '", the filename is already set ("' pars.filename '")']); + end + + % ok, continue + pars.filename = arg; + continue + + end + + % split off option key and option value + if length(arg) < 2 + error('panel:InvalidArgument', ... + ['at argument "' arg '", no option specified']); + end + key = arg(2); + val = arg(3:end); + + % switch on option key + switch key + + case 'p' + pars.paper = validate_par(val, arg, {'A2' 'A3' 'A4' 'A5' 'A6' 'letter' 'LNCS'}); + + case 'l' + pars.landscape = true; + validate_par(val, arg, 'empty'); + + case 'm' + pars.margin = validate_par(str2num(val), arg, 'dimension', 'nonneg'); + + case 'i' + pars.intercolumnspacing = validate_par(str2num(val), arg, 'scalar', 'nonneg'); + + case 'c' + pars.cols = validate_par(str2num(val), arg, 'scalar', 'integer'); + + case 'f' + switch val + case 'a', pars.fill = 1; % all + case 'w', pars.fill = 1; % whole (legacy, not documented) + case 'tt', pars.fill = 2/3; % two thirds + case 'h', pars.fill = 1/2; % half + case 't', pars.fill = 1/3; % third + case 'q', pars.fill = 1/4; % quarter + otherwise + pars.fill = validate_par(str2num(val), arg, 'scalar', [0 1]); + end + + case 'a' + switch val + case 's', pars.fill = -1; % square + case 'g', pars.fill = -1.618; % golden ratio (landscape) + case 'gp', pars.fill = -1/1.618; % golden ratio (portrait) + case 'h', pars.fill = -2; % half height + case 'd', pars.fill = -0.5; % double height + otherwise + pars.fill = -validate_par(str2num(val), arg, 'scalar', [0.1 10]); + end + + case 'w' + pars.width = validate_par(str2num(val), arg, 'scalar', 'nonneg', [10 Inf]); + + case 'h' + pars.height = validate_par(str2num(val), arg, 'scalar', 'nonneg', [10 Inf]); + + case 'r' + % peel off smoothing ("/...") + if any(val == '/') + f = find(val == '/', 1); + switch val(f+1:end) + case '2', pars.smooth = 2; + case '4', pars.smooth = 4; + otherwise, error('panel:InvalidArgument', ... + ['invalid argument "' arg '", part after / must be "2" or "4"']); + end + val = val(1:end-2); + end + + switch val + case 'd', pars.dpi = 75; % draft + case 'n', pars.dpi = 150; % normal + case 'h', pars.dpi = 300; % high + case 'p', pars.dpi = 600; % publication quality + case 'x', pars.dpi = 1200; % extremely high quality + otherwise + pars.dpi = validate_par(str2num(val), arg, 'scalar', [75 2400]); + end + + case 's' + pars.sideways = true; + validate_par(val, arg, 'empty'); + + case 'o' + fmts = { + 'png' 'png' 'png' + 'tif' 'tiff' 'tif' + 'tiff' 'tiff' 'tif' + 'jpg' 'jpeg' 'jpg' + 'jpeg' 'jpeg' 'jpg' + 'ps' 'psc2' 'ps' + 'ps!' 'psc' 'ps' + 'psc' 'psc' 'ps' + 'ps2' 'ps2' 'ps' + 'psc2' 'psc2' 'ps' + 'eps' 'epsc2' 'eps' + 'eps!' 'eps' 'eps' + 'epsc' 'epsc' 'eps' + 'eps2' 'eps2' 'eps' + 'epsc2' 'epsc2' 'eps' + 'pdf' 'pdf' 'pdf' + 'svg' 'svg' 'svg' + }; + validate_par(val, arg, fmts(:, 1)'); + index = isin(fmts(:, 1), val); + pars.fmt = fmts(index, 2:3); + + otherwise + error('panel:InvalidArgument', ... + ['invalid argument "' arg '", option is not recognised']); + + end + + end + + % if not specified, infer format from filename + if isempty(pars.fmt) + [path, base, ext] = fileparts(pars.filename); + if ~isempty(ext) + ext = ext(2:end); + end + switch ext + case {'tif' 'tiff'} + pars.fmt = {'tiff' 'tif'}; + case {'jpg' 'jpeg'} + pars.fmt = {'jpeg' 'jpg'}; + case 'eps' + pars.fmt = {'epsc2' 'eps'}; + case {'png' 'pdf' 'svg'} + pars.fmt = {ext ext}; + case '' + pars.fmt = {'png' 'png'}; + otherwise + warning('panel:CannotInferImageFormat', ... + ['unable to infer image format from file extension "' ext '" (PNG assumed)']); + pars.fmt = {'png' 'png'}; + end + end + + % extract + pars.ext = pars.fmt{2}; + pars.fmt = pars.fmt{1}; + + % extract + is_bitmap = ismember(pars.fmt, {'png' 'jpeg' 'tiff'}); + + % default DPI + if isempty(pars.dpi) + if is_bitmap + pars.dpi = 150; + else + pars.dpi = 600; + end + end + + % validate + if isequal(pars.fmt, 'svg') && isempty(which('plot2svg')) + error('panel:Plot2SVGMissing', 'export to SVG requires plot2svg (Matlab Central File Exchange)'); + end + + % validate + if ~is_bitmap && pars.smooth ~= 1 + pars.smooth = 1; + warning('panel:NoSmoothVectorFormat', 'requested smoothing will not be performed (chosen export format is not a bitmap format)'); + end + + % validate + if isempty(pars.filename) + error('panel:InvalidArgument', 'filename not supplied'); + end + + % make sure filename has extension + if ~any(pars.filename == '.') + pars.filename = [pars.filename '.' pars.ext]; + end + + + +%%%% GET TARGET DIMENSIONS (BEGIN) + + % get space for figure + switch pars.paper + case 'A0', sz = [841 1189]; + case 'A1', sz = [594 841]; + case 'A2', sz = [420 594]; + case 'A3', sz = [297 420]; + case 'A4', sz = [210 297]; + case 'A5', sz = [148 210]; + case 'A6', sz = [105 148]; + case 'letter', sz = [216 279]; + case 'LNCS', sz = [152 235]; + % if margin is still at default, set it to LNCS + % margin size + if isequal(pars.margin, default_margin) + pars.margin = [15 22 15 20]; + end + otherwise + error(['unrecognised paper size "' pars.paper '"']) + end + + % orientation of paper + if pars.landscape + sz = sz([2 1]); + end + + % paper margins (scalar or quad) + if isscalar(pars.margin) + pars.margin = pars.margin * [1 1 1 1]; + end + sz = sz - pars.margin(1:2) - pars.margin(3:4); + + % divide by columns + w = (sz(1) + pars.intercolumnspacing) / pars.cols - pars.intercolumnspacing; + sz(1) = w; + + % apply fill / aspect ratio + if pars.fill > 0 + % fill fraction + sz(2) = sz(2) * pars.fill; + elseif pars.fill < 0 + % aspect ratio + sz(2) = sz(1) * (-1 / pars.fill); + end + + % direct sizing model is used if either of width or height + % is set + if pars.width || pars.height + + % use aspect ratio to fill in either one that is not + % specified + if ~pars.width || ~pars.height + + % aspect ratio must not be a fill + if pars.fill >= 0 + error('cannot use fill fraction with direct sizing model'); + end + + % compute width + if ~pars.width + pars.width = pars.height * -pars.fill; + end + + % compute height + if ~pars.height + pars.height = pars.width / -pars.fill; + end + + end + + % store + sz = [pars.width pars.height]; + + end + +%%%% GET TARGET DIMENSIONS (END) + + + + % orientation of figure is upright, unless printing + % sideways, in which case the printing space is rotated too + if pars.sideways + set(p.h_figure, 'PaperOrientation', 'landscape') + sz = fliplr(sz); + else + set(p.h_figure, 'PaperOrientation', 'portrait') + end + + % report export size + msg = ['exporting to ' int2str(sz(1)) 'x' int2str(sz(2)) 'mm']; + if is_bitmap + psz = sz / 25.4 * pars.dpi; + msg = [msg ' (' int2str(psz(1)) 'x' int2str(psz(2)) 'px @ ' int2str(pars.dpi) 'DPI)']; + else + msg = [msg ' (vector format @ ' int2str(pars.dpi) 'DPI)']; + end + disp(msg); + + % if we are in defer state, we need to do a clean + % recompute first so that axes get positioned so that + % axis ticks get set correctly (if they are in + % automatic mode), since the LAYOUT_MODE_PREPRINT + % recompute will store the tick states. + if p.state.defer + p.state.defer = 0; + p.recomputeLayout([]); + end + + % turn off defer, if it is on + p.state.defer = 0; + + % do a pre-print layout + context.mode = panel.LAYOUT_MODE_PREPRINT; + context.size_in_mm = sz; + context.rect = [0 0 1 1]; + p.recomputeLayout(context); + + % need also to disable the warning that we should set + % PaperPositionMode to auto during this operation - + % we're setting it explicitly. + w = warning('off', 'MATLAB:Print:CustomResizeFcnInPrint'); + + % handle smoothing + pars.write_dpi = pars.dpi; + if pars.smooth > 1 + pars.write_dpi = pars.write_dpi * pars.smooth; + print_filename = [pars.filename '-temp']; + else + print_filename = pars.filename; + end + + % disable layout so it doesn't get computed during any + % figure resize operations that occur during printing. + p.state.defer = 1; + + % set size of figure now. it's important we do this + % after the pre-print layout, because in SVG export + % mode the on-screen figure size is changed and that + % would otherwise affect ticks and limits. + switch pars.fmt + + case 'svg' + % plot2svg (our current SVG export mechanism) uses + % 'Units' and 'Position' (i.e. on-screen position) + % rather than the Paper- prefixed ones used by the + % Matlab export functions. + + % store old on-screen position + svg_units = get(p.h_figure, 'Units'); + svg_pos = get(p.h_figure, 'Position'); + + % update on-screen position + set(p.h_figure, 'Units', 'centimeters'); + pos = get(p.h_figure, 'Position'); + pos(3:4) = sz / 10; + set(p.h_figure, 'Position', pos); + + otherwise + set(p.h_figure, ... + 'PaperUnits', 'centimeters', ... + 'PaperPosition', [0 0 sz] / 10, ... + 'PaperSize', sz / 10 ... % * 1.5 / 10 ... % CHANGED 21/06/2011 so that -opdf works correctly - why was this * 1.5, anyway? presumably was spurious... + ); + + end + + % do fixdash (not for SVG, since plot2svg does a nice + % job of dashed lines without our meddling...) + if ~isequal(pars.fmt, 'svg') + p.do_fixdash(context); + end + + % do the export + switch pars.fmt + case 'svg' + plot2svg(print_filename, p.h_figure); + otherwise + print(p.h_figure, '-loose', ['-d' pars.fmt], ['-r' int2str(pars.write_dpi)], print_filename) + end + + % undo fixdash + if ~isequal(pars.fmt, 'svg') + p.do_fixdash([]); + end + + % set on-screen figure size back to what it was, if it + % was changed. + switch pars.fmt + case 'svg' + set(p.h_figure, 'Units', svg_units); + set(p.h_figure, 'Position', svg_pos); + end + + % enable layout again (it was disabled, above, during + % printing). + p.state.defer = 0; + + % enable warnings + warning(w); + + % do a post-print layout + context.mode = panel.LAYOUT_MODE_POSTPRINT; + context.size_in_mm = []; + context.rect = [0 0 1 1]; + p.recomputeLayout(context); + + % handle smoothing + if pars.smooth > 1 + psz = sz * pars.smooth / 25.4 * pars.dpi; + msg = [' (reducing from ' int2str(psz(1)) 'x' int2str(psz(2)) 'px)']; + disp(['smoothing by factor ' int2str(pars.smooth) msg]); + im1 = imread(print_filename); + delete(print_filename); + sz = size(im1); + sz = [sz(1)-mod(sz(1),pars.smooth) sz(2)-mod(sz(2),pars.smooth)] / pars.smooth; + im = zeros(sz(1), sz(2), 3); + mm = 1:pars.smooth:(sz(1) * pars.smooth); + nn = 1:pars.smooth:(sz(2) * pars.smooth); + for m = 0:pars.smooth-1 + for n = 0:pars.smooth-1 + im = im + double(im1(mm+m, nn+n, :)); + end + end + im = uint8(im / (pars.smooth^2)); + + % set the DPI correctly in the new file + switch pars.fmt + case 'png' + dpm = pars.dpi / 25.4 * 1000; + imwrite(im, pars.filename, ... + 'XResolution', dpm, ... + 'YResolution', dpm, ... + 'ResolutionUnit', 'meter'); + case 'tiff' + imwrite(im, pars.filename, ... + 'Resolution', pars.dpi * [1 1]); + otherwise + imwrite(im, pars.filename); + end + end + + end + + function clearCallbacks(p) + + % clear all callback functions for the panel + % + % p.clearCallbacks() + p.m_callback = {}; + + end + + function setCallback(p, func, userdata) + + % set the callback function for the panel + % + % p.setCallback(myCallbackFunction, userdata) + % + % NB: this function clears all current callbacks, then + % calls addCallback(myCallbackFunction, userdata). + p.clearCallbacks(); + p.addCallback(func, userdata); + + end + + function addCallback(p, func, userdata) + + % attach a callback function to receive panel events + % + % p.addCallback(myCallbackFunction, userdata) + % register myCallbackFunction() to be called when + % events occur on the panel. at present, the only + % event is "layout-updated", which usually occurs + % after the figure is resized. myCallbackFunction() + % should accept one argument, "data", which will + % have the following fields. + % + % "userdata": the userdata passed to this function, if + % any was supplied, else empty. + % + % "panel": a reference to the panel on which the + % callback was set. this object can be queried in + % the usual way. + % + % "event": name of event (currently only + % "layout-updated"). + % + % "context": the layout context for the panel. this + % includes a field "size_in_mm" which is the + % physical size of the rendering surface (screen + % real estate, or image file) and "rect" which is + % the relative size of the rectangle within that + % occupied by the panel which is the context of + % the callback (in [left, bottom, width, height] + % format). + + invalid = ~isscalar(func) || ~isa(func, 'function_handle'); + if invalid + error('panel:InvalidArgument', 'argument to callback() must be a function handle'); + end + if nargin == 2 + p.m_callback{end+1} = {func []}; + else + p.m_callback{end+1} = {func userdata}; + end + + end + + function q = find(p, varargin) + + % find panel according to some search conditions + % + % p.find(...) + % you can use this to recover the panel + % associated with a particular graphics + % object, for example. conditions are + % specified as {type, data} pairs, as listed + % below. + % + % {'object', h} + % returned panels must be managing the object + % "h". + % + % example: + % q = p.find({'object', h_axis}) + + % get all panels + f = p.getPanels('*'); + + % return value + q = {}; + + % search + for i = 1:length(f) + + % get panel + p = f{i}; + + % check conditions + for c = 1:length(varargin) + + % get condition + cond = varargin{c}; + + % switch on type + switch cond{1} + + case 'object' + h = cond{2}; + if ~any(h == p.h_object) + p = []; + end + + otherwise + error(['unrecognised condition type "' cond{1} '"']); + + end + + end + + % if still there + if ~isempty(p) + q{end+1} = p; + end + + end + + end + + function identify(p) + + % add annotations to help identify individual panels + % + % p.identify() + % when creating a complex layout, it can become + % confusing as to which panel is which. this + % function adds a text label to each axis panel + % indicating how to reference the axis panel through + % the root panel. for instance, if "(2, 3)" is + % indicated, you can find that panel at p(2, 3). + % + % see also: show() + + if p.isObject() + + % get managed axes + h_axes = p.getAllManagedAxes(); + + % if no axes, ignore + if isempty(h_axes) + return + end + + % mark first axis + h_axes = h_axes(1); + cla(h_axes); + text(0.5, 0.5, p.state.name, 'fontsize', 12, 'hori', 'center', 'parent', h_axes); + axis(h_axes, [0 1 0 1]); + grid(h_axes, 'off') + + else + + % recurse + for c = 1:length(p.m_children) + p.m_children(c).identify(); + end + + end + + end + + function repack(p, packspec) + + % change the packing specifier for an existing panel + % + % p.repack(packspec) + % repack() is a convenience function provided to + % allow easy development of a layout from the + % command prompt. packspec can be any packing + % specifier accepted by pack(). + % + % see also: pack() + + % deny repack() on root + if p.isRoot() + + % let's deny this. I'm not sure it makes anyway. you + % could always pack into root with a panel with + % absolute positioning, so let's deny first, and + % allow later if we're sure it's a good idea. + error('panel:InvalidArgument', 'root panel cannot be repack()ed'); + + end + + % validate + validate_packspec(packspec); + + % handle units + if iscell(packspec) + units = p.getPropertyValue('units'); + packspec{1} = panel.resolveUnits({packspec{1} units}, 'mm'); + end + + % update the packspec + p.packspec = packspec; + + % and recomputeLayout + p.recomputeLayout([]); + + end + + function pack(p, varargin) + + % add (pack) child panel(s) into an existing panel + % + % p.pack(...) + % add children to the panel "p", committing it as a + % "parent" panel (if it is not already). new (child) + % panels are created using this call - they start as + % "uncommitted" panels. if the parent already has + % children, the new children are appended. The + % following arguments are understood: + % + % 'h'/'v' - switch "p" to pack in the horizontal or + % vertical packing dimension for relative packing + % mode (default for new panels is vertical). + % + % {a, b, c, ...} (a cell row vector) - pack panels + % into "p" with "packing specifiers" a, b, c, etc. + % packing specifiers are detailed below. + % + % PACKING MODES + % panels can be packed into their parent in two + % modes, dependent on their packing specifier. you + % can see a visual representation of these modes on + % the HTML documentation page "Layout". + % + % (i) Relative Mode - panels are packed into the space + % occupied by their parent. size along the parent's + % "packing dimension" is dictated by the packing + % specifier; along the other dimension size matches + % the parent. the following packing specifiers + % indicate Relative Mode. + % + % a) Fixed Size: the specifier is a scalar double in + % a cell {d}. The panel will be of size d in the + % current units of "p" (see the property "p.units" + % for details, but default units are mm). + % + % b) Fractional Size: the specifier is a scalar + % double between 0 and 1 (or between 1 and 100, as a + % percentage). The panel is sized as a fraction of + % the space remaining in its parent after Fixed Size + % panels and inter-panel margins have been subtracted. + % + % c) Stretchable: the specifier is the empty matrix + % []. remaining space in the parent after Fixed and + % Fractional Size panels have been subtracted is + % shared out amongst Stretchable Size panels. + % + % (ii) Absolute Mode - panels hover above their + % parent and do not take up space, as if using the + % position:absolute property in CSS. The packing + % specifier is a 1x4 double vector indicating the + % [left bottom width height] of the panel in + % normalised coordinates of its parent. for example, + % the specifier [0 0 1 1] generates a child panel + % that fills its parent. + % + % SHORTCUTS + % + % ** a small scalar integer, N, (1 to 32) is expanded + % to {[], [], ... []}, with N entries. that is, it + % packs N panels in Relative Mode (Stretchable) and + % shares the available space between them. + % + % ** the call to pack() is recursive, so following a + % packing specifier list, an additional list will + % be used to generate a separate call to pack() on + % each of the children created by the first. hence: + % + % p.pack({[] []}, {[] []}) + % + % will create a 2x2 grid of panels that share the + % space of their parent, "p". since the argument + % "2" expands to {[] []} (see above), the same grid + % can be created using: + % + % p.pack(2, 2) + % + % which is a common idiom in the demos. NB: on + % recursion, the packing dimension is flipped + % automatically, so that a grid is formed. + % + % ** if no arguments are passed at all, a single + % argument {[]} is assumed, so that a single + % additional panel is packed into the parent in + % relative packing mode and with stretchable size. + % + % see also: panel (overview), panel/panel(), select() + % + % LEGACY + % + % the interface to pack() was changed at release + % 2.10 to add support for panels of fixed physical + % size. the interface offered at 2.9 and earlier is + % still available (look inside panel.m - search for + % text "LEGACY" - for details). + + % LEGACY + % + % releases of panel prior to 2.10 did not support + % panels of fixed physical size, and therefore had + % developed a different argument form to that used in + % 2.10 and beyond. specifically, the following + % additional arguments are accepted, for legacy + % support: + % + % 'abs' + % the next argument will be an absolute position, as + % described below. you should avoid using absolute + % positioning mode, in general, since this does not + % take advantage of panel's automatic layout. + % however, on occasion, you may need to take manual + % control of the position of one or more panels. see + % demopanelH for an example. + % + % 1xN row vector (without 'abs') + % pack N new panels along the packing dimension in + % relative mode, with the relative size of each + % given by the elements of the vector. -1 can be + % passed for any elements to mark those panel as + % 'stretchable', so that they fill available space + % left over by other panels packed alongside. the + % sum of the vector (apart from any -1 entries) + % should not come to more than 1, or a warning will + % be generated during laying out. an example would + % be [1/4 1/4 -1], to pack 3 panels, at 25, 25 and + % 50% relative sizes. though, NB, you can use + % percentages instead of fractions if you prefer, in + % which case they should not sum to over 100. so + % that same pack() would be [25 25 -1]. + % + % 1x4 row vector (after 'abs') + % pack 1 new panel using absolute positioning. the + % argument indicates the [left bottom width height] + % of the new panel, in normalised coordinates, as a + % fraction of its parent's position. panels using + % absolute positioning mode are ignored for the sake + % of layout, much like items using + % 'position:absolute' in CSS. + + % handle legacy, parse arguments from varargin into args + args = {}; + while ~isempty(varargin) + + % peel + arg = varargin{1}; + varargin = varargin(2:end); + + % handle shortcut (small integer) on current interface + if isa(arg, 'double') && isscalar(arg) && round(arg) == arg && arg >= 1 && arg <= 32 + arg = cell(1, arg); + end + + % handle current interface - note that the argument + % "recursive" is private and not advertised to the + % user. + if isequal(arg, 'h') || isequal(arg, 'v') || (iscell(arg) && isrow(arg)) || isequal(arg, 'recursive') + args{end+1} = arg; + continue + end + + % report (DEBUG) +% panel.debugmsg('use of LEGACY interface to pack()', 1); + + % handle legacy case + if isequal(arg, 'abs') + if length(varargin) ~= 1 || ~isnumeric(varargin{1}) || ~isofsize(varargin{1}, [1 4]) + error('panel:LegacyAbsNotFollowedBy1x4', 'the argument "abs" on the legacy interface should be followed by a [1x4] row vector'); + end + abs = varargin{1}; + varargin = varargin(2:end); + args{end+1} = {abs}; + continue + end + + % handle legacy case + if isa(arg, 'double') && isrow(arg) + arg_ = {}; + for a = 1:length(arg) + aa = arg(a); + if isequal(aa, -1) + arg_{end+1} = []; + else + arg_{end+1} = aa; + end + end + args{end+1} = arg_; + continue + end + + % unrecognised argument + error('panel:InvalidArgument', 'argument to pack() not recognised'); + + end + + % check m_panelType + if p.isObject() + error('panel:PackWhenObjectPanel', ... + 'cannot pack() into this panel - it is already committed as an object panel'); + end + + % if no arguments, simulate an argument of [], to pack + % a single panel of stretchable size + if isempty(args) + args = {{[]}}; + end + + % state + recursive = false; + + % handle arguments one by one + while ~isempty(args) && ischar(args{1}) + + % extract + arg = args{1}; + args = args(2:end); + + % handle string arguments + switch arg + case 'h' + p.packdim = 1; + case 'v' + p.packdim = 2; + case 'recursive' + recursive = true; + otherwise + error('panel:InvalidArgument', ['pack() did not recognise the argument "' arg '"']); + end + + end + + % if no more arguments that's weird but not bad + if isempty(args) + return + end + + % next argument now must be a cell + arg = args{1}; + args = args(2:end); + if ~iscell(arg) + panel.error('InternalError'); + end + + % commit as parent + p.commitAsParent(); + + % for each element + for i = 1:length(arg) + + % get packspec + packspec = arg{i}; + + % validate + validate_packspec(packspec); + + % handle units + if iscell(packspec) + units = p.getPropertyValue('units'); + packspec{1} = panel.resolveUnits({packspec{1} units}, 'mm'); + end + + % create a child + child = panel('pack', p); + child.packspec = packspec; + + % store it in the parent + if isempty(p.m_children) + p.m_children = child; + else + p.m_children(end+1) = child; + end + + % recurse (further argumens are passed on) + if ~isempty(args) + child_packdim = flippackdim(p.packdim); + edges = 'hv'; + child.pack('recursive', edges(child_packdim), args{:}); + end + + end + + % this must generate a recomputeLayout(), since the + % addition of new panels may affect the layout. any + % recursive call passes 'recursive', so that only the + % root call actually bothers doing a layout. + if ~recursive + p.recomputeLayout([]); + end + + end + + function h_out = select(p, h_object) + + % create or select an axis or object panel + % + % h = p.select(h) + % this call will return the handle of the object + % associated with the panel. if the panel is not yet + % committed, this will involve first committing it + % as an "object panel". if a list of objects ("h") + % is passed, these are the objects associated with + % the panel; if not, a new axis is created by the + % panel when this function is called. + % + % if the object list includes axes, then the "object + % panel" is also known as an "axis panel". in this + % case, the call to select() will make the (first) + % axis current, unless an output argument is + % requested, in which case the handle of the axes + % are returned but no axis is made current. + % + % the passed objects can be user-created axes (e.g. + % a colorbar) or any graphics object that is to have + % its position managed (e.g. a uipanel). your + % mileage may vary with different types of graphics + % object, please let me know. + % + % see also: panel (overview), panel/panel(), pack() + + % handle "all" and "data" + if nargin == 2 && isstring(h_object) && (strcmp(h_object, 'all') || strcmp(h_object, 'data')) + + % collect + h_out = []; + + % commit all uncommitted panels as axis panels by + % selecting them once + if p.isParent() + + % recurse + for c = 1:length(p.m_children) + h_out = [h_out p.m_children(c).select(h_object)]; + end + + elseif p.isUncommitted() + + % select in an axis + h_out = p.select(); + + % plot some data + if strcmp(h_object, 'data') + plot(h_out, randn(100, 1), 'k-'); + end + + end + + % ok + return + + end + + % check m_panelType + if p.isParent() + error('panel:SelectWhenParent', 'cannot select() this panel - it is already committed as a parent panel'); + end + + % commit as object + p.commitAsObject(); + + % assume not a new object + newObject = false; + + % use passed graphics object + if nargin >= 2 + + % validate + if ~all(ishandle(h_object)) + error('panel:InvalidArgument', 'argument to select() must be a list of handles to graphics objects'); + end + + % validate + if ~isempty(p.h_object) + % 02/08/19 I disabled this check because + % I don't see why it's needed (why + % should we not change the managed + % objects on the fly?) +% error('panel:SelectWithObjectWhenObject', 'cannot select() new objects into this panel - it is already managing objects'); + end + + % store + p.h_object = h_object; + newObject = true; + + % make sure it has the correct parent - this doesn't + % seem to affect axes, so we do it for all + set(p.h_object, 'parent', p.h_parent); + + end + + % create new axis if necessary + if isempty(p.h_object) + % 'NextPlot', 'replacechildren' + % make sure fonts etc. don't get changed when user + % plots into it + p.h_object = axes( ... + 'Parent', p.h_parent, ... + 'NextPlot', 'replacechildren' ... + ); + newObject = true; + end + + % if wrapped objects include an axis, and no output args, make it current + h_axes = p.getAllManagedAxes(); + if ~isempty(h_axes) && ~nargout + set(p.h_figure, 'CurrentAxes', h_axes(1)); + + % 12/07/11: this call is slow, because it implies "drawnow" +% figure(p.h_figure); + + % 12/07/11: this call is fast, because it doesn't + set(0, 'CurrentFigure', p.h_figure); + + end + + % and return object list + if nargout + h_out = p.h_object; + end + + % this must generate a applyLayout(), since the axis + % will need positioning appropriately + if newObject + % 09/03/12 mitch + % if there isn't a context yet, we'll have to + % recomputeLayout(), in fact, to generate a context first. + % this will happen, for instance, if a single panel + % is generated in a window that was already open + % (no resize event will fire, and since pack() is + % not called, it will not call recomputeLayout() either). + % nonetheless, we have to reposition this object, so + % this forces us to recomputeLayout() now and generate + % that context we need. + if isempty(p.m_context) + p.recomputeLayout([]); + else + p.applyLayout(); + end + end + + end + + end + + + + + + + + + + + + + + %% ---- HIDDEN OVERLOADS ---- + + methods (Hidden = true) + + function out = vertcat(p, q) + error('panel2:MethodNotImplemented', 'concatenation is not supported by panel (use a cell array instead)'); + end + + function out = horzcat(p, q) + error('panel2:MethodNotImplemented', 'concatenation is not supported by panel (use a cell array instead)'); + end + + function out = cat(dim, p, q) + error('panel2:MethodNotImplemented', 'concatenation is not supported by panel (use a cell array instead)'); + end + + function out = ge(p, q) + error('panel2:MethodNotImplemented', 'inequality operators are not supported by panel'); + end + + function out = le(p, q) + error('panel2:MethodNotImplemented', 'inequality operators are not supported by panel'); + end + + function out = lt(p, q) + error('panel2:MethodNotImplemented', 'inequality operators are not supported by panel'); + end + + function out = gt(p, q) + error('panel2:MethodNotImplemented', 'inequality operators are not supported by panel'); + end + + function out = eq(p, q) + out = eq@handle(p, q); + end + + function out = ne(p, q) + out = ne@handle(p, q); + end + + end + + + + + + + + + + + + + + + + + + + + + + + + + + %% ---- PUBLIC HIDDEN GET/SET ---- + + methods (Hidden = true) + + function p = descend(p, indices) + + while ~isempty(indices) + + % validate + if numel(p) > 1 + error('panel:InvalidIndexing', 'you can only use () on a single (scalar) panel'); + end + + % validate + if ~p.isParent() + error('panel:InvalidIndexing', 'you can only use () on a parent panel'); + end + + % extract + index = indices{1}; + indices = indices(2:end); + + % only accept numeric + if ~isnumeric(index) || ~isscalar(index) + error('panel:InvalidIndexing', 'you can only use () with scalar indices'); + end + + % do the reference + p = p.m_children(index); + + end + + end + + function p_out = subsasgn(p, refs, value) + + % output is always subject + p_out = p; + + % handle () indexing + if strcmp(refs(1).type, '()') + p = p.descend(refs(1).subs); + refs = refs(2:end); + end + + % is that it? + if isempty(refs) + error('panel:InvalidIndexing', 'you cannot assign to a child panel'); + end + + % next ref must be . + if ~strcmp(refs(1).type, '.') + panel.error('InvalidIndexing'); + end + + % either one (.X) or two (.ch.X) + switch numel(refs) + + case 2 + + % validate + if ~strcmp(refs(2).type, '.') + panel.error('InvalidIndexing'); + end + + % validate + switch refs(2).subs + case {'fontname' 'fontsize' 'fontweight'} + case {'margin' 'marginleft' 'marginbottom' 'marginright' 'margintop'} + otherwise + panel.error('InvalidIndexing'); + end + + % avoid computing layout whilst setting descendant + % properties + p.defer(); + + % recurse + switch refs(1).subs + case {'children' 'ch'} + cs = p.m_children; + for c = 1:length(cs) + subsasgn(cs(c), refs(2:end), value); + end + case {'descendants' 'de'} + cs = p.getPanels('*'); + for c = 1:length(cs) + if cs{c} ~= p + subsasgn(cs{c}, refs(2:end), value); + end + end + case {'family' 'fa'} + cs = p.getPanels('*'); + for c = 1:length(cs) + subsasgn(cs{c}, refs(2:end), value); + end + end + + % release for laying out + p.undefer(); + + % mark for appropriate updates + refs(1).subs = refs(2).subs; + + case 1 + + % delegate + p.setPropertyValue(refs(1).subs, value); + + otherwise + panel.error('InvalidIndexing'); + + end + + % update layout as necessary + switch refs(1).subs + case {'fontname' 'fontsize' 'fontweight'} + p.applyLayout('recurse'); + case {'margin' 'marginleft' 'marginbottom' 'marginright' 'margintop'} + p.recomputeLayout([]); + end + + end + + function out = subsref(p, refs) + + % handle () indexing + if strcmp(refs(1).type, '()') + p = p.descend(refs(1).subs); + refs = refs(2:end); + end + + % is that it? + if isempty(refs) + out = p; + return + end + + % next ref must be . + if ~strcmp(refs(1).type, '.') + panel.error('InvalidIndexing'); + end + + % switch on "fieldname" + switch refs(1).subs + + case { ... + 'fontname' 'fontsize' 'fontweight' ... + 'margin' 'marginleft' ... + 'marginbottom' 'marginright' 'margintop' ... + 'units' ... + } + + % delegate this property get + out = p.getPropertyValue(refs(1).subs); + + case 'position' + out = p.getObjectPosition(); + + case 'figure' + out = p.h_figure; + + case 'packspec' + out = p.packspec; + + case 'axis' + if p.isObject() + out = p.getAllManagedAxes(); + else + out = []; + end + + case 'object' + if p.isObject() + h = p.h_object; + ih = ishandle(h); + out = h(ih); + else + out = []; + end + + case {'ch' 'children' 'de' 'descendants' 'fa' 'family'} + + % get the set + switch refs(1).subs + case {'children' 'ch'} + out = {}; + for n = 1:length(p.m_children) + out{n} = p.m_children(n); + end + case {'descendants' 'de'} + out = p.getPanels('*'); + for c = 1:length(out) + if out{c} == p + out = out([1:c-1 c+1:end]); + break + end + end + case {'family' 'fa'} + out = p.getPanels('*'); + end + + % we handle a special case of deeper reference + % here, because we are abusing matlab's syntax to + % do it. other cases (non-abusing) will be handled + % recursively, as usual. this is when we go: + % + % p.ch.axis + % + % which isn't syntactically sound since p.ch is a + % cell array (and potentially a non-singular one + % at that). we re-interpret this to mean + % [p.ch{1}.axis p.ch{2}.axis ...], as follows. + if length(refs) > 1 && isequal(refs(2).type, '.') + switch refs(2).subs + case {'axis' 'object'} + pp = out; + out = []; + for i = 1:length(pp) + out = cat(2, out, subsref(pp{i}, refs(2))); + end + refs = refs(2:end); % used up! + otherwise + % give an informative error message + panel.error('InvalidIndexing'); + end + end + + case { ... + 'addCallback' 'setCallback' 'clearCallbacks' ... + 'hold' ... + 'refresh' 'export' ... + 'pack' 'repack' ... + 'identify' 'show' ... + } + + % validate + if length(refs) ~= 2 || ~strcmp(refs(2).type, '()') + error('panel:InvalidIndexing', ['"' refs(1).subs '" is a function (try "help panel/' refs(1).subs '")']); + end + + % delegate this function call with no output + builtin('subsref', p, refs); + return + + case { ... + 'select' 'fixdash' ... + 'xlabel' 'ylabel' 'zlabel' 'title' ... + 'find' ... + } + + % validate + if length(refs) ~= 2 || ~strcmp(refs(2).type, '()') + error('panel:InvalidIndexing', ['"' refs(1).subs '" is a function (try "help panel/' refs(1).subs '")']); + end + + % delegate this function call with output + if nargout + out = builtin('subsref', p, refs); + else + builtin('subsref', p, refs); + end + return + + otherwise + panel.error('InvalidIndexing'); + + end + + % continue + if length(refs) > 1 + out = subsref(out, refs(2:end)); + end + + end + + end + + + + + + + + + + + + + + %% ---- UTILITY METHODS ---- + + methods (Access = private) + + function b = ismanagefont(p) + + % ask root + b = p.m_root.state.manage_font; + + end + + function b = isdefer(p) + + % ask root + b = p.m_root.state.defer ~= 0; + + end + + function defer(p) + + % increment + p.m_root.state.defer = p.m_root.state.defer + 1; + + end + + function undefer(p) + + % decrement + p.m_root.state.defer = p.m_root.state.defer - 1; + + end + + function cs = getPanels(p, panelTypes, edgespec, all) + + % return all the panels that match the specification. + % + % panelTypes "*": return all panels + % panelTypes "s": return all sizeable panels (parent, + % object and uncommitted) + % panelTypes "p": return only physical panels (object + % or uncommitted) + % panelTypes "o": return only object panels + % + % if edgespec/all is specified, only panels matching + % the edgespec are returned (all of them if "all" is + % true, or any of them - the first one, in fact - if + % "all" is false). + + cs = {}; + + % do not include any that use absolute positioning - + % they stand outside of the sizing model + skip = (numel(p.packspec) == 4) && ~any(panelTypes == '*'); + + if p.isParent() + + % return if appropriate type + if any(panelTypes == '*s') && ~skip + cs = {p}; + end + + % if edgespec was supplied + if nargin == 4 + + % if we are perpendicular to the specified edge + if p.packdim ~= edgespec(1) + + if all + + % return all matching + for c = 1:length(p.m_children) + ppp = p.m_children(c).getPanels(panelTypes, edgespec, all); + cs = cat(2, cs, ppp); + end + + else + + % return only the first one + cs = cat(2, cs, p.m_children(1).getPanels(panelTypes, edgespec, all)); + + end + + else + + % if we are parallel to the specified edge + if edgespec(2) == 2 + + % use last + ppp = p.m_children(end).getPanels(panelTypes, edgespec, all); + cs = cat(2, cs, ppp); + + else + + % use first + cs = cat(2, cs, p.m_children(1).getPanels(panelTypes, edgespec, all)); + + end + + end + + else + + % else, return all + for c = 1:length(p.m_children) + ppp = p.m_children(c).getPanels(panelTypes); + cs = cat(2, cs, ppp); + end + + end + + elseif p.isObject() + + % return if appropriate type + if any(panelTypes == '*spo') && ~skip + cs = {p}; + end + + else + + % return if appropriate type + if any(panelTypes == '*sp') && ~skip + cs = {p}; + end + + end + + end + + function commitAsParent(p) + + if p.isUncommitted() + p.m_panelType = p.PANEL_TYPE_PARENT; + elseif p.isObject() + error('panel:AlreadyCommitted', 'cannot make this panel a parent panel, it is already an object panel'); + end + + end + + function commitAsObject(p) + + if p.isUncommitted() + p.m_panelType = p.PANEL_TYPE_OBJECT; + elseif p.isParent() + error('panel:AlreadyCommitted', 'cannot make this panel an object panel, it is already a parent panel'); + end + + end + + function b = isRoot(p) + + b = isempty(p.parent); + + end + + function b = isParent(p) + + b = p.m_panelType == p.PANEL_TYPE_PARENT; + + end + + function b = isObject(p) + + b = p.m_panelType == p.PANEL_TYPE_OBJECT; + + end + + function b = isUncommitted(p) + + b = p.m_panelType == p.PANEL_TYPE_UNCOMMITTED; + + end + + function h_axes = getAllManagedAxes(p) + + h_axes = []; + for n = 1:length(p.h_object) + h = p.h_object(n); + if isaxis(h) + h_axes = [h_axes h]; + end + end + + end + + function h_object = getOrCreateAxis(p) + + switch p.m_panelType + + case p.PANEL_TYPE_PARENT + + % create if not present + if isempty(p.h_object) + + % 'Visible', 'off' + % this is the hidden axis of a parent panel, + % used for displaying a parent panel's xlabel, + % ylabel and title, but not as a plotting axis + % + % 'NextPlot', 'replacechildren' + % make sure fonts etc. don't get changed when user + % plots into it + p.h_object = axes( ... + 'Parent', p.h_parent, ... + 'Visible', 'off', ... + 'NextPlot', 'replacechildren' ... + ); + + % make sure it's unitary, to help us in + % positioning labels and title + axis(p.h_object, [0 1 0 1]); + + % refresh this axis position + p.applyLayout(); + + end + + % ok + h_object = p.h_object; + + case p.PANEL_TYPE_OBJECT + + % ok + h_object = p.getAllManagedAxes(); + if isempty(h_object) + error('panel:ManagedObjectNotAnAxis', 'this object panel does not manage an axis'); + end + + case p.PANEL_TYPE_UNCOMMITTED + + panel.error('PanelUncommitted'); + + end + + end + + function removeChild(p, child) + + % if not a parent, fail but warn (shouldn't happen) + if ~p.isParent() + warning('panel:NotParentOnRemoveChild', 'i am not a parent (in removeChild())'); + return + end + + % remove from children + for c = 1:length(p.m_children) + if p.m_children(c) == child + p.m_children = p.m_children([1:c-1 c+1:end]); + return + end + end + + % warn + warning('panel:ChildAbsentOnRemoveChild', 'child not found (in removeChild())'); + + end + + function h = getShowAxis(p) + + if p.isRoot() + if isempty(p.h_showAxis) + + % create + p.h_showAxis = axes( ... + 'Parent', p.h_parent, ... + 'units', 'normalized', ... + 'position', [0 0 1 1] ... + ); + + % move to bottom + c = get(p.h_parent, 'children'); + c = [c(2:end); c(1)]; + set(p.h_parent, 'children', c); + + % finalise axis + set(p.h_showAxis, ... + 'xtick', [], 'ytick', [], ... + 'color', 'none', 'box', 'off' ... + ); + axis(p.h_showAxis, [0 1 0 1]); + + % hold + hold(p.h_showAxis, 'on'); + + end + + % return it + h = p.h_showAxis; + + else + h = p.parent.getShowAxis(); + end + + end + + function fireCallbacks(p, event) + + % for each attached callback + for c = 1:length(p.m_callback) + + % extract + callback = p.m_callback{c}; + func = callback{1}; + userdata = callback{2}; + + % fire + data = []; + data.panel = p; + data.event = event; + data.context = p.m_context; + data.userdata = userdata; + func(data); + + end + + end + + end + + + + + + + + + + + + + %% ---- LAYOUT METHODS ---- + + methods + + function refresh(p) + + % recompute layout of all panels + % + % p.refresh() + % recompute the layout of all panels from scratch. + % this should not usually be required, and is + % provided primarily for legacy support. + + % LEGACY + % + % NB: if you pass 'defer' to the constructor, calling + % refresh() both recomputes the layout and releases + % the defer mode. future changes to properties (e.g. + % margins) will cause immediate recomputation of the + % layout, so only call refresh() when you're done. + + % bubble up to root + if ~p.isRoot() + p.m_root.refresh(); + return + end + + % release defer + p.state.defer = 0; + + % debug output +% panel.debugmsg(['refresh "' p.state.name '"...']); + + % call recomputeLayout + p.recomputeLayout([]); + + end + + end + + methods (Access = private) + + function do_fixdash(p, context) + + % if context is [], this is _after_ the layout for + % export, so we need to restore + if isempty(context) + + % restore lines we changed to their original state + for r = 1:length(p.m_fixdash_restore) + + % get + restore = p.m_fixdash_restore{r}; + + % if empty, no change was made + if ~isempty(restore) + set(restore.h_line, ... + 'xdata', restore.xdata, 'ydata', restore.ydata); + delete([restore.h_supp restore.h_mark]); + end + + end + + else + +% % get handles to objects that still exist +% h_lines = p.m_fixdash(ishandle(p.m_fixdash)); + + % no restores + p.m_fixdash_restore = {}; + + % for each line + for i = 1:length(p.m_fixdash) + + % get + fix = p.m_fixdash{i}; + + % final check + if ~ishandle(fix.h) || ~isequal(get(fix.h, 'type'), 'line') + continue + end + + % apply dashstyle + p.m_fixdash_restore{end+1} = dashstyle_line(fix, context); + + end + + end + + end + + function p = recomputeLayout(p, context) + + % this function recomputes the layout from scratch. + % this means calculating the sizes of the root panel + % and all descendant panels. after this is completed, + % the function calls applyLayout to effect the new + % layout. + + % if not root, bubble up to root + if ~p.isRoot() + p.m_root.recomputeLayout(context); + return + end + + % if in defer mode, do not compute layout + if p.isdefer() + return + end + + % if no context supplied (e.g. on resize events), use + % the figure window (a context is supplied if + % exporting to an image file). + if isempty(context) + context.mode = panel.LAYOUT_MODE_NORMAL; + context.size_in_mm = []; + context.rect = [0 0 1 1]; + end + + % debug output +% panel.debugmsg(['recomputeLayout "' p.state.name '"...']); + +% % root may have a packspec of its own +% if ~isempty(p.packspec) +% if isscalar(p.packspec) +% % this should never happen, because it should be +% % caught when the packspec is set in repack() +% warning('panel:RootPanelCannotUseRelativeMode', 'the root panel uses relative positioning mode - this is ignored'); +% else +% context.rect = p.packspec; +% end +% end + + % if not given a context size, use the size on screen + % of the parent figure + if isempty(context.size_in_mm) + + % get context (whole parent) size in its units + pp = get(p.h_figure, 'position'); + context_size = pp(3:4); + + % defaults, in case this fails for any reason + screen_size = [1280 1024]; + if ismac + screen_dpi = 72; + else + screen_dpi = 96; + end + + % get screen DPI + try + local_screen_dpi = get(0, 'ScreenPixelsPerInch'); + if ~isempty(local_screen_dpi) + screen_dpi = local_screen_dpi; + end + end + + % get screen size + try + local_screen_size = get(0, 'ScreenSize'); + if ~isempty(local_screen_size) + screen_size = local_screen_size; + end + end + + % get figure width and height on screen + switch get(p.h_figure, 'Units') + + case 'points' + points_per_inch = 72; + context.size_in_mm = context_size / points_per_inch * 25.4; + + case 'inches' + context.size_in_mm = context_size * 25.4; + + case 'centimeters' + context.size_in_mm = context_size * 10.0; + + case 'pixels' + context.size_in_mm = context_size / screen_dpi * 25.4; + + case 'characters' + context_size = context_size .* [5 13]; % convert to pixels (based on empirical measurement) + context.size_in_mm = context_size / screen_dpi * 25.4; + + case 'normalized' + context_size = context_size .* screen_size(3:4); % convert to pixels (based on screen size) + context.size_in_mm = context_size / screen_dpi * 25.4; + + otherwise + error('panel:CaseNotCoded', ['case not coded, (Parent Units are ' get(p.h_figure, 'Units') ')']); + + end + + end + + % that's the figure size, now we need the size of our + % parent, if it's not the figure too + if p.h_parent ~= p.h_figure + units = get(p.h_parent, 'units'); + set(p.h_parent, 'units', 'normalized'); + pos = get(p.h_parent, 'position'); + set(p.h_parent, 'units', units); + context.size_in_mm = context.size_in_mm .* pos(3:4); + end + + % for the root, we apply the margins here, since it's + % a special case because there's always exactly one of + % it + margin = p.getPropertyValue('margin', 'mm'); + m = margin([1 3]) / context.size_in_mm(1); + context.rect = context.rect + [m(1) 0 -sum(m) 0]; + m = margin([2 4]) / context.size_in_mm(2); + context.rect = context.rect + [0 m(1) 0 -sum(m)]; + + % now, recurse + p.recurseComputeLayout(context); + + % clear h_showAxis when we recompute the layout + if ~isempty(p.h_showAxis) + delete(p.h_showAxis); + p.h_showAxis = []; + end + + % having computed the layout, we now apply it, + % starting at the root panel. + p.applyLayout('recurse'); + + end + + function recurseComputeLayout(p, context) + + % store context + p.m_context = context; + + % if no children, do nothing further + if isempty(p.m_children) + return + end + + % else, we're going to recompute the layout for our + % children + margins = []; + + % get size to pack into + mm_canvas = context.size_in_mm(p.packdim); + mm_context = mm_canvas * context.rect(2+p.packdim); + + % get list of children that are packed relative - we + % do this because the computation only handles these + % relative children; absolute packed children are + % ignored through the computation, and are just packed + % as specified when the time comes. + rel_list = []; + + % for each child + for i = 1:length(p.m_children) + + % get child + c = p.m_children(i); + + % is it packed abs? + if isofsize(c.packspec, [1 4]) + continue + end + + % if not, it's packed relative, so add to list + rel_list(end+1) = i; + + end + + % array of actual sizes as fraction of parent (note we + % only represent the rel_list). + zz = zeros(1, length(rel_list)); + sz_phys = zz; + sz_frac = zz; + i_stretch = zz; + + % for each child that is packed relative + for i = 1:length(rel_list) + + % get child + c = p.m_children(rel_list(i)); + + % get internal margin + margin = c.getPropertyValue('margin', 'mm'); + if p.packdim == 2 + margin = margin([2 4]); + margin = fliplr(margin); % doclink FLIP_PACKDIM_2 - same reason, here! + else + margin = margin([1 3]); + end + margins(i:i+1, i) = margin'; + + % subtract fixed size packspec from packing size + if iscell(c.packspec) + % NB: fixed size is always _stored_ in mm! + sz_phys(i) = c.packspec{1}; + end + + % get relative packing sizes + if isnumeric(c.packspec) && isscalar(c.packspec) + % NB: relative size is a scalar numeric + sz_frac(i) = c.packspec; + % convert perc to frac + if sz_frac(i) > 1 + sz_frac(i) = sz_frac(i) / 100; + end + end + + % get stretch packing size + if isempty(c.packspec) + % NB: these will be filled later + i_stretch(i) = 1; + end + + % else, it's an abs packing size, and we can ignore + % it for this phase of layout + + end + + % finalise internal margins (that is, the margin at + % each boundary between two adjacent relative packed + % panels is the maximum of the margins specified by + % each of the pair). + margins = max(margins, [], 2); + margins = margins(2:end-1)'; + + % subtract internal margins to give available space + % for objects (in mm) + mm_objects = mm_context - sum(margins); + + % now, subtract physically sized objects to give + % available space to share out amongst panels that + % specify their size as a fraction. + mm_share = mm_objects - sum(sz_phys); + + % and now stretch items can be given their actual + % fractional size, since we now know who they are + % sharing space with. + sz_frac(find(i_stretch)) = (1 - sum(sz_frac)) / sum(i_stretch); + + % and we can now get the real physical size of all the + % fractionally-sized panels in mm. + sz_frac = sz_frac * mm_share; + + % finally, we've got the physical boundaries of + % everything; let's just tidy that up. + sz = [[sz_phys + sz_frac]; margins 0]; + sz = sz(1:end-1); + + % and let's normalise the physical boundaries, because + % we're actually going to specify them to matlab in + % normalised form, even though we computed them in mm. + if ~isempty(sz) + + % do it + sz_norm = reshape([0 cumsum(sz / mm_context)]', 2, [])'; + + % for packdim 2, we pack from the top, whereas + % matlab's position property packs from the bottom, so + % we have to flip these. doclink FLIP_PACKDIM_2. + if p.packdim == 2 + sz_norm = fliplr(1 - sz_norm); + end + + end + + % recurse + for i = 1:length(p.m_children) + + % get child + c = p.m_children(i); + + % handle abs packed panels + if isofsize(c.packspec, [1 4]) + + % child context + child_context = context; + rect = child_context.rect; + rect([1 3]) = c.packspec([1 3]) * rect(3) + [rect(1) 0]; + rect([2 4]) = c.packspec([2 4]) * rect(4) + [rect(2) 0]; + child_context.rect = rect; + + else + + % child context + child_context = context; + rr = sz_norm(1, :); + sz_norm = sz_norm(2:end, :); % sz_norm has only as many entries as there are rel-packed panels + ri = p.packdim + [0 2]; + a = child_context.rect(ri(1)); + b = child_context.rect(ri(2)); + child_context.rect(ri) = [a+rr(1)*b diff(rr)*b]; + + end + + % recurse + c.recurseComputeLayout(child_context); + + end + + end + + function applyLayout(p, varargin) + + % this function applies the layout that is stored in + % each panel objects "m_context" member, and fixes up + % the position of any associated objects (such as axis + % group labels). + + % skip if disabled + if p.isdefer() + return + end + + % debug output +% panel.debugmsg(['applyLayout "' p.state.name '"...']); + + % defaults + recurse = false; + + % handle arguments + while ~isempty(varargin) + + % get + arg = varargin{1}; + varargin = varargin(2:end); + + % handle + switch arg + case 'recurse' + recurse = true; + otherwise + panel.error('InternalError'); + end + + end + + % recurse + if recurse + pp = p.getPanels('*'); + else + pp = {p}; + end + + % why do we have to split the applyLayout() operation + % into two? + % + % because the "group labels" are positioned with + % respect to the axes in their group depending on + % whether those axes have tick labels, and what those + % tick labels are. if those tick labels are in + % automatic mode (as they usually are), they may + % change when those axes are positioned. since an axis + % group may contain many of these nested deep, we have + % to position all axes (step 1) first, then (step 2) + % position any group labels. + + % step 1 + for pi = 1:length(pp) + pp{pi}.applyLayout1(); + end + + % step 2 + for pi = 1:length(pp) + pp{pi}.applyLayout2(); + end + + % callbacks + for pi = 1:length(pp) + fireCallbacks(pp{pi}, 'layout-updated'); + end + + end + + function r = getObjectPosition(p) + + % get packed position + r = p.m_context.rect; + + % if empty, must be absolute position + if isempty(r) + r = p.packspec; + pp = getObjectPosition(p.parent); + r = panel.getRectangleOfRectangle(pp, r); + end + + end + + function applyLayout1(p) + + % if no context yet, skip this call + if isempty(p.m_context) + return + end + + % if no managed objects, skip this call + if isempty(p.h_object) + return + end + + % debug output +% panel.debugmsg(['applyLayout1 "' p.state.name '"...']); + + % handle LAYOUT_MODE + switch p.m_context.mode + + case panel.LAYOUT_MODE_PREPRINT + + % if in LAYOUT_MODE_PREPRINT, store current axis + % layout (ticks and ticklabels) and lock them into + % manual mode so they don't get changed during the + % print operation + h_axes = p.getAllManagedAxes(); + for n = 1:length(h_axes) + p.state.store{n} = storeAxisState(h_axes(n)); + end + + case panel.LAYOUT_MODE_POSTPRINT + + % if in LAYOUT_MODE_POSTPRINT, restore axis + % layout, leaving it as it was before we ran + % export + h_axes = p.getAllManagedAxes(); + for n = 1:length(h_axes) + restoreAxisState(h_axes(n), p.state.store{n}); + end + + end + + % position it + try + set(p.h_object, 'position', p.getObjectPosition(), 'units', 'normalized'); + catch err + if strcmp(err.identifier, 'MATLAB:hg:set_chck:DimensionsOutsideRange') + w = warning('query', 'backtrace'); + warning off backtrace + warning('panel:PanelZeroSize', 'a panel had zero size, and the managed object was hidden'); + set(p.h_object, 'position', [-0.3 -0.3 0.2 0.2]); + if strcmp(w.state, 'on') + warning on backtrace + end + elseif strcmp(err.identifier, 'MATLAB:class:InvalidHandle') + % this will happen if the user deletes the managed + % objects manually. an obvious way that this + % happens is if the user select()s some panels so + % that axes get created, then calls clf. it would + % be nice if we could clear the panels attached to + % a figure in response to a clf call, but there + % doesn't seem any obvious way to pick up the clf + % call, only the delete(objects) that follows, and + % this is indistinguishable from a call by the + % user to delete(my_axis), for instance. how are + % we to respond if the user deletes the axis the + % panel is managing? it's not clear. so, we'll + % just fail silently, for now, and these panels + % will either never be used again (and will be + % destroyed when the figure is closed) or will be + % destroyed when the user creates a new panel on + % this figure. either way, i think, no real harm + % done. +% w = warning('query', 'backtrace'); +% warning off backtrace +% warning('panel:PanelObjectDestroyed', 'the object managed by a panel has been destroyed'); +% if strcmp(w.state, 'on') +% warning on backtrace +% end +% panel.debugmsg('***WARNING*** the object managed by a panel has been destroyed'); + return + else + rethrow(err) + end + end + + % if managing fonts + if p.ismanagefont() + + % apply properties to objects + h = p.h_object; + + % get those which are axes + h_axes = p.getAllManagedAxes(); + + % and labels/title objects, for any that are axes + for n = 1:length(h_axes) + h = [h ... + get(h_axes(n), 'xlabel') ... + get(h_axes(n), 'ylabel') ... + get(h_axes(n), 'zlabel') ... + get(h_axes(n), 'title') ... + ]; + end + + % apply font properties + set(h, ... + 'fontname', p.getPropertyValue('fontname'), ... + 'fontsize', p.getPropertyValue('fontsize'), ... + 'fontweight', p.getPropertyValue('fontweight') ... + ); + + end + + end + + function applyLayout2(p) + + % if no context yet, skip this call + if isempty(p.m_context) + return + end + + % if no object, skip this call + if isempty(p.h_object) + return + end + + % if not a parent, skip this call + if ~p.isParent() + return + end + + % if not an axis, skip this call - NB: this is not a + % displayed and managed object, rather it is the + % invisible axis used to display parent labels/titles. + % we checked above if this panel is a parent. thus, + % the member h_object must be scalar, if it is + % non-empty. + if ~isaxis(p.h_object) + return + end + + % debug output +% panel.debugmsg(['applyLayout2 "' p.state.name '"...']); + + % matlab moves x/ylabels around depending on + % whether the axis in question has any x/yticks, + % so that the label is always "near" the axis. + % we try to do the same, but it's hack-o-rama. + + % calibration offsets - i measured these + % empirically, what a load of shit + font_fudge = [2 1/3]; + nofont_fudge = [2 0]; + + % do xlabel + cs = p.getPanels('o', [2 2], true); + y = 0; + for c = 1:length(cs) + ch = cs{c}; + h_axes = ch.getAllManagedAxes(); + for h_axis = h_axes + % only if there are some tick labels, and they're + % at the bottom... + if ~isempty(get(h_axis, 'xticklabel')) && ~isempty(get(h_axis, 'xtick')) ... + && strcmp(get(h_axis, 'xaxislocation'), 'bottom') + fontoffset_mm = get(h_axis, 'fontsize') * font_fudge(2) + font_fudge(1); + y = max(y, fontoffset_mm); + end + end + end + y = max(y, get(p.h_object, 'fontsize') * nofont_fudge(2) + nofont_fudge(1)); + + % convert and lay in + axisheight_mm = p.m_context.size_in_mm(2) * p.m_context.rect(4); + y = y / axisheight_mm; + set(get(p.h_object, 'xlabel'), ... + 'VerticalAlignment', 'Cap', ... + 'Units', 'Normalized', ... + 'Position', [0.5 -y 1]); + + % calibration offsets - i measured these + % empirically, what a load of shit + font_fudge = [3 1/6]; + nofont_fudge = [2 0]; + + % do ylabel + cs = p.getPanels('o', [1 1], true); + x = 0; + for c = 1:length(cs) + ch = cs{c}; + h_axes = ch.getAllManagedAxes(); + for h_axis = h_axes + % only if there are some tick labels, and they're + % at the left... + if ~isempty(get(h_axis, 'yticklabel')) && ~isempty(get(h_axis, 'ytick')) ... + && strcmp(get(h_axis, 'yaxislocation'), 'left') + yt = get(h_axis, 'yticklabel'); + if ischar(yt) + ml = size(yt, 2); + else + ml = 0; + for i = 1:length(yt) + ml = max(ml, length(yt{i})); + end + end + fontoffset_mm = get(h_axis, 'fontsize') * ml * font_fudge(2) + font_fudge(1); + x = max(x, fontoffset_mm); + end + end + end + x = max(x, get(p.h_object, 'fontsize') * nofont_fudge(2) + nofont_fudge(1)); + + % convert and lay in + axisheight_mm = p.m_context.size_in_mm(1) * p.m_context.rect(3); + x = x / axisheight_mm; + set(get(p.h_object, 'ylabel'), ... + 'VerticalAlignment', 'Bottom', ... + 'Units', 'Normalized', ... + 'Position', [-x 0.5 1]); + + % calibration offsets - made up based on the + % ones i measured for the labels + nofont_fudge = [2 0]; + + % get y position + y = max(y, get(p.h_object, 'fontsize') * nofont_fudge(2) + nofont_fudge(1)); + + % convert and lay in + axisheight_mm = p.m_context.size_in_mm(2) * p.m_context.rect(4); + y = y / axisheight_mm; + set(get(p.h_object, 'title'), ... + 'VerticalAlignment', 'Bottom', ... + 'Position', [0.5 1+y 1]); + + % 21/11/19 move to bottom of z-index stack so + % that it does not interfere with mouse + % interactions with the other axes (e.g. + % zooming) + uistack(p.h_object, 'bottom') + + end + + end + + + + + + + %% ---- PROPERTY METHODS ---- + + methods (Access = private) + + function value = getPropertyValue(p, key, units) + + value = p.prop.(key); + + if isempty(value) + + % inherit + if ~isempty(p.parent) + switch key + case {'fontname' 'fontsize' 'fontweight' 'margin' 'units'} + if nargin == 3 + value = p.parent.getPropertyValue(key, units); + else + value = p.parent.getPropertyValue(key); + end + return + end + end + + % default + if isempty(value) + value = panel.getPropertyDefault(key); + end + + end + + % translate dimensions + switch key + case {'margin'} + if nargin < 3 + units = p.getPropertyValue('units'); + end + value = panel.resolveUnits(value, units); + end + + end + + function setPropertyValue(p, key, value) + + % root properties + switch key + case 'units' + if ~isempty(p.parent) + p.parent.setPropertyValue(key, value); + return + end + end + + % value validation + switch key + case 'units' + invalid = ~( (isstring(value) && isin({'mm' 'in' 'cm' 'pt'}, value)) || isempty(value) ); + case 'fontname' + invalid = ~( isstring(value) || isempty(value) ); + case 'fontsize' + invalid = ~( (isnumeric(value) && isscalar(value) && value >= 4 && value <= 60) || isempty(value) ); + case 'fontweight' + invalid = ~( (isstring(value) && isin({'normal' 'bold'}, value)) || isempty(value) ); + case 'margin' + invalid = ~( (isdimension(value)) || isempty(value) ); + case {'marginleft' 'marginbottom' 'marginright' 'margintop'} + invalid = ~isscalardimension(value); + otherwise + error('panel:UnrecognisedProperty', ['unrecognised property "' key '"']); + end + + % value validation + if invalid + error('panel:InvalidValueForProperty', ['invalid value for property "' key '"']); + end + + % marginX properties + switch key + case {'marginleft' 'marginbottom' 'marginright' 'margintop'} + index = isin({'left' 'bottom' 'right' 'top'}, key(7:end)); + element = value; + value = p.getPropertyValue('margin'); + value(index) = element; + key = 'margin'; + end + + % translate dimensions + switch key + case {'margin'} + if isscalar(value) + value = value * [1 1 1 1]; + end + if ~isempty(value) + units = p.getPropertyValue('units'); + value = {panel.resolveUnits({value units}, 'mm') 'mm'}; + end + end + + % lay in + p.prop.(key) = value; + + end + + end + + methods (Static = true, Access = private) + + function s = fignum(h) + + % handled differently pre/post 2014b + if isa(h, 'matlab.ui.Figure') + % R2014b + s = num2str(h.Number); + else + % pre-R2014b + s = num2str(h); + end + end + + function prop = getPropertyInitialState() + + prop = panel.getPropertyDefaults(); + for key = fieldnames(prop)' + prop.(key{1}) = []; + end + + end + + function value = getPropertyDefault(key) + + persistent defprop + + if isempty(defprop) + defprop = panel.getPropertyDefaults(); + end + + value = defprop.(key); + + end + + function defprop = getPropertyDefaults() + + % root properties + defprop.units = 'mm'; + + % inherited properties + defprop.fontname = get(0, 'defaultAxesFontName'); + defprop.fontsize = get(0, 'defaultAxesFontSize'); + defprop.fontweight = 'normal'; + defprop.margin = {[15 15 5 5] 'mm'}; + + % not inherited properties + % CURRENTLY, NONE! +% defprop.align = false; + + end + + end + + + + + + + + %% ---- STATIC PUBLIC METHODS ---- + + methods (Static = true) + + function p = recover(h_figure) + + % get a handle to the root panel associated with a figure + % + % p = recover(h_fig) + % if you have not got a handle to the root panel of + % the figure h_fig, this call will retrieve it. if + % h_fig is not supplied, gcf is used. + + if nargin < 1 + h_figure = gcf; + end + + p = panel.callbackDispatcher('recover', h_figure); + + end + + function version() + + % report the version of panel that is active + % + % panel.version() + + fid = fopen(which('panel')); + tag = '% Release Version'; + ltag = length(tag); + tagline = 'Unable to determine Release Version'; + while 1 + line = fgetl(fid); + if ~ischar(line) + break + end + if length(line) > ltag && strcmp(line(1:ltag), tag) + tagline = line(3:end); + end + end + fclose(fid); + disp(tagline) + + end + + function panic() + + % call delete on all children of the global workspace, + % to recover from bugs that leave us with uncloseable + % figures. call this as "panel.panic()". + % + % NB: if you have to call panic(), something has gone + % wrong. if you are able to reproduce the problem, + % please contact me to report the bug. + delete(allchild(0)); + + end + + end + + + + + + + %% ---- STATIC PRIVATE METHODS ---- + + methods (Static = true, Access = private) + + function error(id) + + switch id + case 'PanelUncommitted' + throwAsCaller(MException('panel:PanelUncommitted', 'this action cannot be performed on an uncommitted panel')); + case 'InvalidIndexing' + throwAsCaller(MException('panel:InvalidIndexing', 'you cannot index a panel object in this way')); + case 'InternalError' + throwAsCaller(MException('panel:InternalError', 'an internal error occurred')); + otherwise + throwAsCaller(MException('panel:UnknownError', ['an unknown error was generated with id "' id '"'])); + end + + end + + function lockClass() + + persistent hasLocked + if isempty(hasLocked) + + % only lock if not in debug mode + if ~panel.isDebug() + % in production code, must mlock() file at this point, + % to avoid persistent variables being cleared by user + if strcmp(getenv('USERDOMAIN'), 'BERGEN') + % my machine, do nothing + else + mlock + end + end + + % mark that we've handled this + hasLocked = true; + + end + + end + + function debugmsg(msg, focus) + + % focus can be supplied to force only focussed + % messages to be shown + if nargin < 2 + focus = 1; + end + + % display, if in debug mode + if focus + if panel.isDebug() + disp(msg); + end + end + + end + + function state = isDebug() + + % persistent + persistent debug + + % create + if isempty(debug) + try + debug = panel_debug_state(); + catch + debug = false; + end + end + + % ok + state = debug; + + end + + function r = getFractionOfRectangle(r, dim, range) + + switch dim + case 1 + r = [r(1)+range(1)*r(3) r(2) range(2)*r(3) r(4)]; + case 2 + r = [r(1) r(2)+(1-sum(range))*r(4) r(3) range(2)*r(4)]; + otherwise + error('panel:CaseNotCoded', ['case not coded, dim = ' dim ' (internal error)']); + end + + end + + function r = getRectangleOfRectangle(r, s) + + w = r(3); + h = r(4); + r = [r(1)+s(1)*w r(2)+s(2)*h s(3)*w s(4)*h]; + + end + + function a = getUnionRect(a, b) + + if isempty(a) + a = b; + end + if ~isempty(b) + d = a(1) - b(1); + if d > 0 + a(1) = a(1) - d; + a(3) = a(3) + d; + end + d = a(2) - b(2); + if d > 0 + a(2) = a(2) - d; + a(4) = a(4) + d; + end + d = b(1) + b(3) - (a(1) + a(3)); + if d > 0 + a(3) = a(3) + d; + end + d = b(2) + b(4) - (a(2) + a(4)); + if d > 0 + a(4) = a(4) + d; + end + end + + end + + function r = reduceRectangle(r, margin) + + r(1:2) = r(1:2) + margin(1:2); + r(3:4) = r(3:4) - margin(1:2) - margin(3:4); + + end + + function v = normaliseDimension(v, space_size_in_mm) + + v = v ./ [space_size_in_mm space_size_in_mm]; + + end + + function v = resolveUnits(d, units) + + % first, convert into mm + v = d{1}; + switch d{2} + case 'mm' + % ok + case 'cm' + v = v * 10.0; + case 'in' + v = v * 25.4; + case 'pt' + v = v / 72.0 * 25.4; + otherwise + error('panel:CaseNotCoded', ['case not coded, storage units = ' units ' (internal error)']); + end + + % then, convert to specified units + switch units + case 'mm' + % ok + case 'cm' + v = v / 10.0; + case 'in' + v = v / 25.4; + case 'pt' + v = v / 25.4 * 72.0; + otherwise + error('panel:CaseNotCoded', ['case not coded, requested units = ' units ' (internal error)']); + end + + end + + function resizeCallback(obj, evt) + + panel.callbackDispatcher('resize', obj); + + end + + function closeCallback(obj, evt) + + panel.callbackDispatcher('delete', obj); + delete(obj); + + end + + function out = callbackDispatcher(op, data) + + % debug output +% panel.debugmsg(['callbackDispatcher(' op ')...']) + + % persistent store + persistent registeredPanels + + % switch on operation + switch op + + case {'register' 'registerNoClear'} + + % if a root panel is already attached to this + % figure, we could throw an error and refuse to + % create the new object, we could delete the + % existing panel, or we could allow multiple + % panels to be attached to the same figure. + % + % we should allow multiple panels, because they + % may have different parents within the same + % figure (e.g. uipanels). but by default we don't, + % unless the panel.add() static constructor is + % used. + + if strcmp(op, 'register') + + argument_h_figure = data.h_figure; + i = 0; + while i < length(registeredPanels) + i = i + 1; + if registeredPanels(i).h_figure == argument_h_figure + delete(registeredPanels(i)); + i = 0; + end + end + + end + + % register the new panel + if isempty(registeredPanels) + registeredPanels = data; + else + registeredPanels(end+1) = data; + end + + % debug output +% panel.debugmsg(['panel registered (' int2str(length(registeredPanels)) ' now registered)']); + + case 'unregister' + + % debug output +% panel.debugmsg(['on unregister, ' int2str(length(registeredPanels)) ' registered']); + + for r = 1:length(registeredPanels) + if registeredPanels(r) == data + registeredPanels = registeredPanels([1:r-1 r+1:end]); + + % debug output +% panel.debugmsg(['panel unregistered (' int2str(length(registeredPanels)) ' now registered)']); + + return + end + end + + % warn + warning('panel:AbsentOnCallbacksUnregister', 'panel was absent from the callbacks register when it tried to unregister itself'); + + case 'resize' + + argument_h_parent = data; + for r = 1:length(registeredPanels) + if registeredPanels(r).h_parent == argument_h_parent + registeredPanels(r).recomputeLayout([]); + end + end + + case 'recover' + + argument_h_figure = data; + out = []; + for r = 1:length(registeredPanels) + if registeredPanels(r).h_figure == argument_h_figure + if isempty(out) + out = registeredPanels(r); + else + out(end+1) = registeredPanels(r); + end + end + end + + case 'delete' + + argument_h_figure = data; + i = 0; + while i < length(registeredPanels) + i = i + 1; + if registeredPanels(i).h_figure == argument_h_figure + delete(registeredPanels(i)); + i = 0; + end + end + + end + + end + + end + + + + +end + + + + + + + + + + + + + + + + + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% HELPERS +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +function restore = dashstyle_line(fix, context) + +% get axis size in mm +h_line = fix.h; +h_axis = get(h_line, 'parent'); +u = get(h_axis, 'units'); +set(h_axis, 'units', 'norm'); +pos = get(h_axis, 'position'); +set(h_axis, 'units', u); +axis_in_mm = pos(3:4) .* context.size_in_mm; + +% recover data +xdata = get(h_line, 'xdata'); +ydata = get(h_line, 'ydata'); +zdata = get(h_line, 'zdata'); +linestyle = get(h_line, 'linestyle'); +marker = get(h_line, 'marker'); + +% empty restore +restore = []; + +% do not handle 3D +if ~isempty(zdata) + warning('panel:NoFixdash3D', 'panel cannot fixdash() a 3D line - no action taken'); + return +end + +% get range of axis +ax = axis(h_axis); + +% get scale in each dimension (mm per unit) +sc = axis_in_mm ./ (ax([2 4]) - ax([1 3])); + +% create empty line +data = NaN; + +% override linestyle +if ~isempty(fix.linestyle) + linestyle = fix.linestyle; +end + +% transcribe linestyle +linestyle = dashstyle_parse_linestyle(linestyle); +if isempty(linestyle) + return +end + +% scale +scale = 1; +dashes = linestyle * scale; + +% store for restore +restore.h_line = h_line; +restore.xdata = xdata; +restore.ydata = ydata; + +% create another, separate, line to overlay on the original +% line and render the fixed-up dashes. +restore.h_supp = copyobj(h_line, h_axis); + +% if the original line has markers, we'll have to create yet +% another separate line instance to represent them, because +% they shouldn't be "dashed", as it were. note that we don't +% currently attempt to get the z-order right for these +% new lines. +if ~isequal(marker, 'none') + restore.h_mark = copyobj(h_line, h_axis); + set(restore.h_mark, 'linestyle', 'none'); + set(restore.h_supp, 'marker', 'none'); +else + restore.h_mark = []; +end + +% hide the original line. this line remains in existence so +% that if there is a legend, it doesn't get messed up. +set(h_line, 'xdata', NaN, 'ydata', NaN); + +% extract pattern length +patlen = sum(dashes); + +% position within pattern is initially zero +pos = 0; + +% linedata +line_xy = complex(xdata, ydata); + +% for each line segment +while length(line_xy) > 1 + + % get line segment + xy = line_xy(1:2); + line_xy = line_xy(2:end); + + % any NaNs, and we're outta here + if any(isnan(xy)) + continue + end + + % get start etc. + O = xy(1); + V = xy(2) - xy(1); + + % get mm length of this line segment + d = sqrt(sum(([real(V) imag(V)] .* sc) .^ 2)); + + % and mm unit vector + U = V / d; + + % generate a long-enough pattern for this segment + n = ceil((pos + d) / patlen); + pat = [0 cumsum(repmat(dashes, [1 n]))] - pos; + pos = d - (pat(end) - patlen); + pat = [pat(1:2:end-1); pat(2:2:end)]; + + % trim spurious segments + pat = pat(:, any(pat >= 0) & any(pat <= d)); + + % skip if that's it + if isempty(pat) + continue + end + + % and reduce ones that are oversized + pat(1) = max(pat(1), 0); + pat(end) = min(pat(end), d); + + % finally, add these segments to the line data + seg = [O + pat * U; NaN(1, size(pat, 2))]; + data = [data seg(:).']; + +end + +% update line +set(restore.h_supp, 'xdata', real(data), 'ydata', imag(data), ... + 'linestyle', '-'); + +end + + +function linestyle = dashstyle_parse_linestyle(linestyle) + +if isequal(linestyle, 'none') || isequal(linestyle, '-') + linestyle = []; + return +end + +while 1 + + % if numbers + if isnumeric(linestyle) + if ~isa(linestyle, 'double') || ~isrow(linestyle) || mod(length(linestyle), 2) ~= 0 + break + end + % no need to parse + return + end + + % else, must be char + if ~ischar(linestyle) || ~isrow(linestyle) + break + end + + % translate matlab non-standard codes into codes we can + % easily parse + switch linestyle + case ':' + linestyle = '.'; + case '--' + linestyle = '-'; + end + + % must be only - and . + if any(linestyle ~= '.' & linestyle ~= '-') + break + end + + % transcribe + c = linestyle; + linestyle = []; + for l = c + switch l + case '-' + linestyle = [linestyle 2 0.75]; + case '.' + linestyle = [linestyle 0.5 0.75]; + end + end + return + +end + +warning('panel:BadFixdashLinestyle', 'unusable linestyle in fixdash()'); +linestyle = []; + +end + + + +% MISCELLANEOUS + +function index = isin(list, value) + +for i = 1:length(list) + if strcmp(value, list{i}) + index = i; + return + end +end + +index = 0; + +end + +function dim = flippackdim(dim) + +% this function, used between arguments in a recursive call, +% causes the dim to be switched with each recurse, so that +% we build a grid, rather than a long, long row. +dim = 3 - dim; + +end + + + +% STRING PADDING FUNCTIONS + +function s = rpad(s, l) + +if nargin < 2 + l = 16; +end + +if length(s) < l + s = [s repmat(' ', 1, l - length(s))]; +end + +end + +function s = lpad(s, l) + +if nargin < 2 + l = 16; +end + +if length(s) < l + s = [repmat(' ', 1, l - length(s)) s]; +end + +end + + + +% HANDLE GRAPHICS HELPERS + +function h = getParentFigure(h) + +if strcmp(get(h, 'type'), 'figure') + return +else + h = getParentFigure(get(h, 'parent')); +end + +end + +function addHandleCallback(h, name, func) + +% % get current list of callbacks +% callbacks = get(h, name); +% +% % if empty, turn into a cell +% if isempty(callbacks) +% callbacks = {}; +% elseif iscell(callbacks) +% % only add ourselves once +% for c = 1:length(callbacks) +% if callbacks{c} == func +% return +% end +% end +% else +% callbacks = {callbacks}; +% end +% +% % and add ours (this is friendly, in case someone else has a +% % callback attached) +% callbacks{end+1} = func; +% +% % lay in +% set(h, name, callbacks); + +% the above isn't as simple as i thought - for now, we'll +% just stamp on any existing callbacks +set(h, name, func); + +end + +function store = storeAxisState(h) + +% LOCK TICKS AND LIMITS +% +% (LOCK TICKS) +% +% lock state so that the ticks and labels do not change when +% the figure is resized for printing. this is what the user +% will expect, which is why we go through this palaver. +% +% however, for fuck's sake. the following code illustrates +% an idiosyncrasy of matlab (i would call this an +% inconsistency, myself, but there you go). +% +% figure +% axis([0 1 0 1]) +% set(gca, 'ytick', [-1 0 1 2]) +% get(gca, 'yticklabel') +% set(gca, 'yticklabelmode', 'manual') +% +% now, resize the figure window. at least in R2011b, the +% tick labels change on the first resize event. presumably, +% this is because matlab treats the ticklabel value +% differently depending on if the ticklabelmode is auto or +% manual. if it's manual, the value is used as documented, +% and [0 1] is used to label [-1 0 1 2], cyclically. +% however, if the ticklabelmode is auto, and the ticks +% extend outside the figure, then the ticklabels are set +% sensibly, but the _value_ of ticklabel is not consistent +% with what it would need to be to get this tick labelling +% were the mode manual. and, in a final bizarre twist, this +% doesn't become evident until the resize event. i think +% this is a bug, no other way of looking at it; at best it's +% an inconsistency that is either tedious or impossible to +% work around in the general case. +% +% in any case, we have to lock the ticks to manual as we go +% through the print cycle, so that the ticks do not get +% changed if they were in automatic mode. but we mustn't fix +% the tick labels to manual, since if we do we may encounter +% this inconsistency and end up with the wrong tick labels +% in the print out. i can't, at time of writing, think of a +% case where we'd have to fix the tick labels to manual too. +% the possible cases are: +% +% ticks auto, labels auto: in this case, fixing the ticks to +% manual should be enough. +% +% ticks manual, labels auto: leave as is. +% +% ticks manual, labels manual: leave as is. +% +% the only other case is ticks auto, labels manual, which is +% a risky case to use, but in any case we can also fix the +% ticks to manual in that case. thus, our preferred solution +% is to always switch the ticks to manual, if they're not +% already, and otherwise leave things be. +% +% (LOCK LIMITS) +% +% the other thing that may get modified, if the user hasn't +% fixed it, is the axis limits. so we lock them too, any +% that are set to auto, and mark them for unlocking when the +% print is complete. + +store = ''; + +% manual-ise ticks on any axis where they are currently +% automatic, and indicate that we need to switch them back +% afterwards. +if strcmp(get(h, 'XTickMode'), 'auto') + store = [store 'X']; + set(h, 'XTickMode', 'manual'); +end +if strcmp(get(h, 'YTickMode'), 'auto') + store = [store 'Y']; + set(h, 'YTickMode', 'manual'); +end +if strcmp(get(h, 'ZTickMode'), 'auto') + store = [store 'Z']; + set(h, 'ZTickMode', 'manual'); +end + +% manual-ise limits on any axis where they are currently +% automatic, and indicate that we need to switch them back +% afterwards. +if strcmp(get(h, 'XLimMode'), 'auto') + store = [store 'x']; + set(h, 'XLimMode', 'manual'); +end +if strcmp(get(h, 'YLimMode'), 'auto') + store = [store 'y']; + set(h, 'YLimMode', 'manual'); +end +if strcmp(get(h, 'ZLimMode'), 'auto') + store = [store 'z']; + set(h, 'ZLimMode', 'manual'); +end + +% % OLD CODE OBSOLETED 25/01/12 - see notes above +% +% % store current state +% store.XTick = get(h, 'XTick'); +% store.XTickMode = get(h, 'XTickMode'); +% store.XTickLabel = get(h, 'XTickLabel'); +% store.XTickLabelMode = get(h, 'XTickLabelMode'); +% store.YTickMode = get(h, 'YTickMode'); +% store.YTick = get(h, 'YTick'); +% store.YTickLabel = get(h, 'YTickLabel'); +% store.YTickLabelMode = get(h, 'YTickLabelMode'); +% store.ZTick = get(h, 'ZTick'); +% store.ZTickMode = get(h, 'ZTickMode'); +% store.ZTickLabel = get(h, 'ZTickLabel'); +% store.ZTickLabelMode = get(h, 'ZTickLabelMode'); +% +% % lock state to manual +% set(h, 'XTickLabelMode', 'manual'); +% set(h, 'XTickMode', 'manual'); +% set(h, 'YTickLabelMode', 'manual'); +% set(h, 'YTickMode', 'manual'); +% set(h, 'ZTickLabelMode', 'manual'); +% set(h, 'ZTickMode', 'manual'); + +end + +function restoreAxisState(h, store) + +% unmanualise +for item = store + switch item + case {'X' 'Y' 'Z'} + set(h, [item 'TickMode'], 'auto'); + case {'x' 'y' 'z'} + set(h, [upper(item) 'TickMode'], 'auto'); + end +end + +% % OLD CODE OBSOLETED 25/01/12 - see notes above +% +% % restore passed state +% set(h, 'XTick', store.XTick); +% set(h, 'XTickMode', store.XTickMode); +% set(h, 'XTickLabel', store.XTickLabel); +% set(h, 'XTickLabelMode', store.XTickLabelMode); +% set(h, 'YTick', store.YTick); +% set(h, 'YTickMode', store.YTickMode); +% set(h, 'YTickLabel', store.YTickLabel); +% set(h, 'YTickLabelMode', store.YTickLabelMode); +% set(h, 'ZTick', store.ZTick); +% set(h, 'ZTickMode', store.ZTickMode); +% set(h, 'ZTickLabel', store.ZTickLabel); +% set(h, 'ZTickLabelMode', store.ZTickLabelMode); + +end + + + +% DIM AND EDGE HANDLING + +% we describe each edge of a panel in terms of "dim" (1 or +% 2, horizontal or vertical) and "edge" (1 or 2, former or +% latter). together, [dim edge] is an "edgespec". + +function s = edgestr(edgespec) + +s = 'lbrt'; +s = s(edgeindex(edgespec)); + +end + +function i = edgeindex(edgespec) + +% edge indices. margins are stored as [l b r t] but +% dims are packed left to right and top to bottom, so +% relationship between 'dim' and 'end' and index into +% margin is non-trivial. we call the index into the margin +% the "edgeindex". an "edgespec" is just [dim end], in a +% single array. +i = [1 3; 4 2]; +i = i(edgespec(1), edgespec(2)); + +end + + + +% VARIABLE TYPE HELPERS + +function val = validate_par(val, argtext, varargin) + +% this helper validates arguments to some functions in the +% main body + +for n = 1:length(varargin) + + % get validation constraint + arg = varargin{n}; + + % handle string list + if iscell(arg) + % string list + if ~isin(arg, val) + error('panel:InvalidArgument', ... + ['invalid argument "' argtext '", "' val '" is not a recognised data value for this option']); + end + continue; + end + + % handle strings + if isstring(arg) + switch arg + case 'empty' + if ~isempty(val) + error('panel:InvalidArgument', ... + ['invalid argument "' argtext '", option does not expect any data']); + end + case 'dimension' + if ~isdimension(val) + error('panel:InvalidArgument', ... + ['invalid argument "' argtext '", option expects a dimension']); + end + case 'scalar' + if ~(isnumeric(val) && isscalar(val) && ~isnan(val)) + error('panel:InvalidArgument', ... + ['invalid argument "' argtext '", option expects a scalar value']); + end + case 'nonneg' + if any(val(:) < 0) + error('panel:InvalidArgument', ... + ['invalid argument "' argtext '", option expects non-negative values only']); + end + case 'integer' + if any(val(:) ~= round(val(:))) + error('panel:InvalidArgument', ... + ['invalid argument "' argtext '", option expects integer values only']); + end + end + continue; + end + + % handle numeric range + if isnumeric(arg) && isofsize(arg, [1 2]) + if any(val(:) < arg(1)) || any(val(:) > arg(2)) + error('panel:InvalidArgument', ... + ['invalid argument "' argtext '", option data must be between ' num2str(arg(1)) ' and ' num2str(arg(2))]); + end + continue; + end + + % not recognised + arg + error('panel:InternalError', 'internal error - bad argument to validate_par (above)'); + +end + +end + +function b = checkpar(value, mn, mx) + +b = isscalar(value) && isnumeric(value) && ~isnan(value); +if b + if nargin >= 2 + b = b && value >= mn; + end + if nargin >= 3 + b = b && value <= mx; + end +end + +end + +function b = isintegral(v) + +b = all(all(v == round(v))); + +end + +function b = isstring(value) + +sz = size(value); +b = ischar(value) && length(sz) == 2 && sz(1) == 1 && sz(2) >= 1; + +end + +function b = isdimension(value) + +b = isa(value, 'double') && (isscalar(value) || isofsize(value, [1 4])); + +end + +function b = isscalardimension(value) + +b = isa(value, 'double') && isscalar(value); + +end + +function b = isofsize(value, siz) + +sz = size(value); +b = length(sz) == length(siz) && all(sz == siz); + +end + +function b = isaxis(h) + +b = ishandle(h) && strcmp(get(h, 'type'), 'axes'); + +end + +function validate_packspec(packspec) + + % stretchable + if isempty(packspec) + return + end + + % scalar + if isa(packspec, 'double') && isscalar(packspec) + + % fraction + if packspec > 0 && packspec <= 1 + return + end + + % percentage + if packspec > 1 && packspec <= 100 + return + end + + end + + % fixed + if iscell(packspec) && isscalar(packspec) + + % delve + d = packspec{1}; + if isa(d, 'double') && isscalar(d) && d > 0 + return + end + + end + + % abs + if isa(packspec, 'double') && isofsize(packspec, [1 4]) && all(packspec(3:4)>0) + return + end + + % otherwise, bad form + error('panel:BadPackingSpecifier', 'the packing specifier was not valid - see help panel/pack'); + +end + + + diff --git a/lib/slice_display b/lib/slice_display new file mode 160000 index 00000000..4326779c --- /dev/null +++ b/lib/slice_display @@ -0,0 +1 @@ +Subproject commit 4326779c8e9d7681e0b13827196aad64c801e170 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/manualTests/miss_hit.cfg b/manualTests/miss_hit.cfg new file mode 100644 index 00000000..684e5239 --- /dev/null +++ b/manualTests/miss_hit.cfg @@ -0,0 +1 @@ +regex_function_name: "(test(_unit){0,1}_[a-z]+|[a-z]+)(([A-Z]){1}[A-Za-z0-9]+)*" diff --git a/manualTests/test_bidsSegmentSkullStrip.m b/manualTests/test_bidsSegmentSkullStrip.m deleted file mode 100644 index f10ea4b2..00000000 --- a/manualTests/test_bidsSegmentSkullStrip.m +++ /dev/null @@ -1,29 +0,0 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - -function test_suite = test_bidsSegmentSkullStrip %#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_bidsSegmentSkullStripBasic() - - % smoke test - - % directory with this script becomes the current directory - opt.dataDir = fullfile( ... - fileparts(mfilename('fullpath')), ... - '..', 'demos', 'MoAE', 'output', 'MoAEpilot'); - % task to analyze - opt.taskName = 'auditory'; - opt = checkOptions(opt); - - %% Run batches - checkDependencies(); - reportBIDS(opt); - bidsCopyRawFolder(opt, 1); - bidsSegmentSkullStrip(opt); - -end diff --git a/manualTests/systemTestDS114.m b/manualTests/test_ds114.m similarity index 99% rename from manualTests/systemTestDS114.m rename to manualTests/test_ds114.m index b9dfb487..b9c18231 100644 --- a/manualTests/systemTestDS114.m +++ b/manualTests/test_ds114.m @@ -1,5 +1,5 @@ % (C) Copyright 2019 Remi Gau - +% % Script to run to check that the whole pipeline works fine with different % options encoded in json files % diff --git a/manualTests/systemTestMoAE.m b/manualTests/test_moae.m similarity index 65% rename from manualTests/systemTestMoAE.m rename to manualTests/test_moae.m index a2b87437..94f470a7 100644 --- a/manualTests/systemTestMoAE.m +++ b/manualTests/test_moae.m @@ -1,5 +1,5 @@ % (C) Copyright 2019 Remi Gau - +% % Script to run to check that the whole pipeline works fine with different % options encoded in json files % @@ -17,6 +17,8 @@ % Smoothing to apply FWHM = 6; +downloadData = true; + % URL of the data set to download URL = 'http://www.fil.ion.ucl.ac.uk/spm/download/data/MoAEpilot/MoAEpilot.bids.zip'; @@ -24,24 +26,11 @@ WD = fullfile(fileparts(mfilename('fullpath')), '..', 'demos', 'MoAE'); cd(WD); -% we add all the subfunctions that are in the sub directories -addpath(genpath(fullfile(WD, '..', '..', 'src'))); -addpath(genpath(fullfile(WD, '..', '..', 'lib'))); - -%% 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'); +run ../../initCppSpm.m; -checkDependencies(); +download_moae_ds(downloadData); %% Set up -delete(fullfile(pwd, 'options_task-*date-*.json')); - optionsFilesList = { ... 'options_task-auditory.json'; ... 'options_task-auditory_unwarp-0.json'; ... @@ -53,7 +42,7 @@ fprintf(1, repmat('\n', 1, 5)); - optionJsonFile = optionsFilesList{iOption}; + optionJsonFile = fullfile(WD, 'options', optionsFilesList{iOption}); opt = loadAndCheckOptions(optionJsonFile); %% Run batches @@ -66,17 +55,23 @@ bidsSpatialPrepro(opt); - % The following do not run on octave for now (because of spmup) - anatomicalQA(opt); + % Some of the following do not run on octave for now (because of spmup) + % Crashes in CI + % anatomicalQA(opt); + bidsResliceTpmToFunc(opt); - functionalQA(opt); + + % Crashes in CI + % functionalQA(opt); bidsSmoothing(FWHM, opt); + bidsSegmentSkullStrip(opt); + % The following crash on Travis CI - bidsFFX('specifyAndEstimate', opt, FWHM); - bidsFFX('contrasts', opt, FWHM); - bidsResults(opt, FWHM); + % bidsFFX('specifyAndEstimate', opt, FWHM); + % bidsFFX('contrasts', opt, FWHM); + % bidsResults(opt, FWHM); cd(WD); diff --git a/manualTests/test_setBatchCoregistrationFmap.m b/manualTests/test_setBatchCoregistrationFmap.m index eaa1f88c..4d055052 100644 --- a/manualTests/test_setBatchCoregistrationFmap.m +++ b/manualTests/test_setBatchCoregistrationFmap.m @@ -1,4 +1,4 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers +% (C) Copyright 2020 CPP_SPM developers function test_suite = test_setBatchCoregistrationFmap %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 diff --git a/manualTests/test_setBatchCreateVDMs.m b/manualTests/test_setBatchCreateVDMs.m index 314008e5..e87e6350 100644 --- a/manualTests/test_setBatchCreateVDMs.m +++ b/manualTests/test_setBatchCreateVDMs.m @@ -1,4 +1,4 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers +% (C) Copyright 2020 CPP_SPM developers function test_suite = test_setBatchCreateVDMs %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 diff --git a/miss_hit.cfg b/miss_hit.cfg index 2c6a2b5c..3c2173ce 100644 --- a/miss_hit.cfg +++ b/miss_hit.cfg @@ -1,14 +1,18 @@ # styly guide (https://florianschanda.github.io/miss_hit/style_checker.html) line_length: 100 -regex_function_name: "[a-z]+(_*([a-zA-Z0-9]){1}[A-Za-z]+)*" # almost anything goes in the root folder + +regex_function_name: "[a-z]+([a-zA-Z0-9]){1}[a-zA-Z0-9]+" +regex_script_name: "[a-z0-9]+(_[a-z0-9]+)*" + exclude_dir: "lib" + copyright_entity: "JH" copyright_entity: "DSS" copyright_entity: "Agah Karakuzu" copyright_entity: "Olivier Collignon" copyright_entity: "Mohamed Rezk" copyright_entity: "Remi Gau" -copyright_entity: "CPP BIDS SPM-pipeline developers" +copyright_entity: "CPP_SPM developers" tab_width: 2 diff --git a/npm-requirements.txt b/npm-requirements.txt deleted file mode 100644 index 061f9ade..00000000 --- a/npm-requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -remark-cli@5.0.0 -remark-lint@6.0.2 -remark-preset-lint-recommended@3.0.2 -remark-preset-lint-markdown-style-guide@2.1.2 -remark-preset-lint-consistent - diff --git a/requirements.txt b/requirements.txt index 992d0ece..578e596a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ Sphinx sphinxcontrib-matlabdomain sphinxcontrib-napoleon sphinx_rtd_theme -miss_hit==0.9.15 \ No newline at end of file +miss_hit==0.9.20 \ No newline at end of file diff --git a/runTests.m b/run_tests.m similarity index 91% rename from runTests.m rename to run_tests.m index ac531bf4..98dc6821 100644 --- a/runTests.m +++ b/run_tests.m @@ -1,4 +1,4 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developpers +% (C) Copyright 2019 CPP_SPM developers warning('OFF'); diff --git a/src/QA/anatomicalQA.m b/src/QA/anatomicalQA.m index 25d70fa9..3e689e8c 100644 --- a/src/QA/anatomicalQA.m +++ b/src/QA/anatomicalQA.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function anatomicalQA(opt) % % Computes several metrics for anatomical image. @@ -11,6 +9,7 @@ function anatomicalQA(opt) % :param opt: Options chosen for the analysis. See ``checkOptions()``. % :type opt: structure % + % (C) Copyright 2020 CPP_SPM developers if isOctave() warning('\nanatomicalQA is not yet supported on Octave. This step will be skipped.'); @@ -23,43 +22,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) - - groupName = group(iGroup).name; - - parfor iSub = 1:group(iGroup).numSub + parfor iSub = 1:numel(opt.subjects) - subID = group(iGroup).subNumber{iSub}; + subID = opt.subjects{iSub}; - printProcessingSubject(groupName, iSub, subID); + printProcessingSubject(iSub, subID); - [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); + [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); - % get grey and white matter tissue probability maps - TPMs = validationInputFile(anatDataDir, anatImage, 'c[12]'); + % get grey and white matter tissue probability maps + TPMs = validationInputFile(anatDataDir, anatImage, 'c[12]'); - % 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))); + % 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))); - % 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> + % 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> - anatQA.avgDistToSurf = spmup_comp_dist2surf(anatImage); + anatQA.avgDistToSurf = spmup_comp_dist2surf(anatImage); - spm_jsonwrite( ... - strrep(anatImage, '.nii', '_qa.json'), ... - anatQA, ... - struct('indent', ' ')); + 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..f74ff38c 100644 --- a/src/QA/functionalQA.m +++ b/src/QA/functionalQA.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function functionalQA(opt) % % For functional data, QA consists in getting temporal SNR and then @@ -23,6 +21,8 @@ function functionalQA(opt) % - the tissue probability maps have been generated in the "native" space of each subject % (using ``bidsSpatialPrepro()`` or ``bidsSegmentSkullStrip()``) and have been % resliced to the dimension of the functional with ``bidsResliceTpmToFunc()`` + % + % (C) Copyright 2020 CPP_SPM developers if isOctave() warning('\nfunctionalQA is not yet supported on Octave. This step will be skipped.'); @@ -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) - - groupName = group(iGroup).name; - - for iSub = 1:group(iGroup).numSub - - subID = group(iGroup).subNumber{iSub}; + for iSub = 1:numel(opt.subjects) - printProcessingSubject(groupName, iSub, subID); + subID = opt.subjects{iSub}; - % get grey and white matter and csf tissue probability maps - [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); - TPMs = validationInputFile(anatDataDir, anatImage, 'rc[123]'); + printProcessingSubject(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); - % create carpet plot + createDataDictionary(subFuncDataDir, fileName, size(confounds, 2)); - % 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/setBatch3Dto4D.m b/src/batches/setBatch3Dto4D.m index 90e761bb..d9811282 100644 --- a/src/batches/setBatch3Dto4D.m +++ b/src/batches/setBatch3Dto4D.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function matlabbatch = setBatch3Dto4D(matlabbatch, volumesList, RT, outputName, dataType) % % Set the batch for 3D to 4D conversion @@ -31,6 +29,7 @@ % - 16: FLOAT32 - single prec. float % - 64: FLOAT64 - double prec. float % + % (C) Copyright 2020 CPP_SPM developers if nargin < 5 || isempty(dataType) dataType = 0; diff --git a/src/batches/setBatchComputeVDM.m b/src/batches/setBatchComputeVDM.m index 00321ae4..dddf02bb 100644 --- a/src/batches/setBatchComputeVDM.m +++ b/src/batches/setBatchComputeVDM.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function matlabbatch = setBatchComputeVDM(matlabbatch, fmapType, refImage) % % Short description of what the function does goes here. @@ -21,6 +19,8 @@ % % adapted from spmup get_FM_workflow.m (@ commit % 198c980d6d7520b1a996f0e56269e2ceab72cc83) + % + % (C) Copyright 2020 CPP_SPM developers switch lower(fmapType) case 'phasediff' diff --git a/src/batches/setBatchContrasts.m b/src/batches/setBatchContrasts.m index ea047235..89162fe7 100644 --- a/src/batches/setBatchContrasts.m +++ b/src/batches/setBatchContrasts.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function matlabbatch = setBatchContrasts(matlabbatch, spmMatFile, consess) % % Short description of what the function does goes here. @@ -17,6 +15,7 @@ % % :returns: - :matlabbatch: (structure) % + % (C) Copyright 2019 CPP_SPM developers printBatchName('contrasts specification'); diff --git a/src/batches/setBatchCoregistration.m b/src/batches/setBatchCoregistration.m index 8fb4d51f..5efa1477 100644 --- a/src/batches/setBatchCoregistration.m +++ b/src/batches/setBatchCoregistration.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function matlabbatch = setBatchCoregistration(matlabbatch, ref, src, other) % % Set the batch for coregistering the source images into the reference image @@ -21,6 +19,7 @@ % % matlabbatch = setBatchCoregistrationGeneral(matlabbatch, ref, src, other) % + % (C) Copyright 2020 CPP_SPM developers printBatchName('coregistration'); diff --git a/src/batches/setBatchCoregistrationFmap.m b/src/batches/setBatchCoregistrationFmap.m index ca220f41..2d1bcab7 100644 --- a/src/batches/setBatchCoregistrationFmap.m +++ b/src/batches/setBatchCoregistrationFmap.m @@ -1,48 +1,47 @@ -% (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 % - % TODO % - implement for 'phase12', 'fieldmap', 'epi' + % + % (C) Copyright 2020 CPP_SPM developers printBatchName('coregister fieldmaps data to functional'); % 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 +49,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..70b4761a 100644 --- a/src/batches/setBatchCoregistrationFuncToAnat.m +++ b/src/batches/setBatchCoregistrationFuncToAnat.m @@ -1,6 +1,4 @@ -% (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,22 +14,23 @@ % :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 % + % (C) Copyright 2020 CPP_SPM developers printBatchName('coregister functional data to anatomical'); matlabbatch{end + 1}.spm.spatial.coreg.estimate.ref(1) = ... - cfg_dep('Named File Selector: Anatomical(1) - Files', ... - substruct( ... - '.', 'val', '{}', {opt.orderBatches.selectAnat}, ... - '.', 'val', '{}', {1}, ... - '.', 'val', '{}', {1}, ... - '.', 'val', '{}', {1}), ... - substruct('.', 'files', '{}', {1})); + cfg_dep('Named File Selector: Anatomical(1) - Files', ... + substruct( ... + '.', 'val', '{}', {opt.orderBatches.selectAnat}, ... + '.', 'val', '{}', {1}, ... + '.', 'val', '{}', {1}, ... + '.', 'val', '{}', {1}), ... + substruct('.', 'files', '{}', {1})); % SOURCE IMAGE : DEPENDENCY FROM REALIGNEMENT % Mean Image @@ -53,14 +52,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/setBatchCreateVDMs.m b/src/batches/setBatchCreateVDMs.m index 54f4a08e..9851ed7f 100644 --- a/src/batches/setBatchCreateVDMs.m +++ b/src/batches/setBatchCreateVDMs.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function matlabbatch = setBatchCreateVDMs(matlabbatch, BIDS, opt, subID) % % Short description of what the function does goes here. @@ -22,6 +20,8 @@ % % TODO % - implement for 'phase12', 'fieldmap', 'epi' + % + % (C) Copyright 2020 CPP_SPM developers printBatchName('create voxel displacement map'); diff --git a/src/batches/setBatchEstimateModel.m b/src/batches/setBatchEstimateModel.m index 73601a25..de0ff6bf 100644 --- a/src/batches/setBatchEstimateModel.m +++ b/src/batches/setBatchEstimateModel.m @@ -1,6 +1,4 @@ -% (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. % @@ -15,10 +13,11 @@ % % :returns: - :matlabbatch: (structure) % + % (C) Copyright 2019 CPP_SPM developers switch nargin - case 1 + case 2 printBatchName('estimate subject level fmri model'); @@ -30,7 +29,7 @@ '.', 'val', '{}', {1}), ... substruct('.', 'spmmat')); - matlabbatch = returnEstimateModelBatch(matlabbatch, spmMatFile); + matlabbatch = returnEstimateModelBatch(matlabbatch, spmMatFile, opt); case 3 @@ -42,7 +41,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 +53,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.glm.QA.do + writeResiduals = false(); + end + matlabbatch{end}.spm.stats.fmri_est.write_residuals = writeResiduals; + end diff --git a/src/batches/setBatchFactorialDesign.m b/src/batches/setBatchFactorialDesign.m index f183b9d4..c063e68b 100644 --- a/src/batches/setBatchFactorialDesign.m +++ b/src/batches/setBatchFactorialDesign.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function matlabbatch = setBatchFactorialDesign(matlabbatch, opt, funcFWHM, conFWHM) % % Short description of what the function does goes here. @@ -19,6 +17,7 @@ % % :returns: - :matlabbatch: (structure) % + % (C) Copyright 2019 CPP_SPM developers printBatchName('specify group level fmri model'); @@ -28,7 +27,7 @@ smoothPrefix = ['s', num2str(conFWHM)]; end - [group, opt] = getData(opt); + [~, opt] = getData(opt); rfxDir = getRFXdir(opt, funcFWHM, conFWHM); @@ -54,34 +53,27 @@ mkdir(directory); - % For each group - for iGroup = 1:length(group) - - groupName = group(iGroup).name; - - icell(iGroup).levels = iGroup; %#ok<*AGROW> - - for iSub = 1:group(iGroup).numSub + icell(1).levels = 1; %#ok<*AGROW> - subID = group(iGroup).subNumber{iSub}; + for iSub = 1:numel(opt.subjects) - printProcessingSubject(groupName, iSub, subID); + subLabel = opt.subjects{iSub}; - % FFX directory and load SPM.mat of that subject - ffxDir = getFFXdir(subID, funcFWHM, opt); - load(fullfile(ffxDir, 'SPM.mat')); + printProcessingSubject(iSub, subLabel); - % 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); + % FFX directory and load SPM.mat of that subject + ffxDir = getFFXdir(subLabel, funcFWHM, opt); + load(fullfile(ffxDir, 'SPM.mat')); - icell(iGroup).scans(iSub, :) = {file}; + % 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); - fprintf(1, ' %s\n\n', file); + icell(1).scans(iSub, :) = {file}; - end + fprintf(1, ' %s\n\n', file); 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/batches/setBatchGroupLevelContrasts.m b/src/batches/setBatchGroupLevelContrasts.m index 5d83966c..f6346c4c 100644 --- a/src/batches/setBatchGroupLevelContrasts.m +++ b/src/batches/setBatchGroupLevelContrasts.m @@ -1,6 +1,6 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function matlabbatch = setBatchGroupLevelContrasts(matlabbatch, grpLvlCon, rfxDir) + % + % (C) Copyright 2019 CPP_SPM developers printBatchName('group level contrast estimation'); diff --git a/src/batches/setBatchImageCalculation.m b/src/batches/setBatchImageCalculation.m index 771aee1e..e350d6b6 100644 --- a/src/batches/setBatchImageCalculation.m +++ b/src/batches/setBatchImageCalculation.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function matlabbatch = setBatchImageCalculation(matlabbatch, input, output, outDir, expression) % % Set a batch for a image calculation @@ -21,6 +19,7 @@ % % :returns: - :matlabbatch: % + % (C) Copyright 2020 CPP_SPM developers printBatchName('image calculation'); diff --git a/src/batches/setBatchLesionAbnormalitiesDetection.m b/src/batches/setBatchLesionAbnormalitiesDetection.m new file mode 100755 index 00000000..8ea61493 --- /dev/null +++ b/src/batches/setBatchLesionAbnormalitiesDetection.m @@ -0,0 +1,41 @@ +function matlabbatch = setBatchLesionAbnormalitiesDetection(matlabbatch, BIDS, opt, subLabel) + % + % Creates a batch to detect lesion abnormalities + % + % USAGE:: + % + % matlabbatch = setBatchLesionAbnormalitiesDetection(matlabbatch, BIDS, opt, subLabel) + % + % :param matlabbatch: list of SPM batches + % :type matlabbatch: structure + % + % :returns: - :matlabbatch: (structure) + % + % (C) Copyright 2021 CPP_SPM developers + + printBatchName('Lesion abnormalities'); + + % Defin smoothed segmented images of patients + matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3patients = ''; + + % Defin smoothed segmented images of neurotypical controls + matlabbatch{1}.spm.tools.ali.outliers_detection.step3tissue.step3controls = ''; + + % 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; + + % 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) + + matlabbatch{end + 1}.spm.tools.ali.outliers_detection = outliers_detection; + +end diff --git a/src/batches/setBatchLesionOverlapMap.m b/src/batches/setBatchLesionOverlapMap.m new file mode 100755 index 00000000..3f7dfc37 --- /dev/null +++ b/src/batches/setBatchLesionOverlapMap.m @@ -0,0 +1,19 @@ +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) + % + % (C) Copyright 2021 CPP_SPM developers + + 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 new file mode 100755 index 00000000..858cea1d --- /dev/null +++ b/src/batches/setBatchLesionSegmentation.m @@ -0,0 +1,40 @@ +function matlabbatch = setBatchLesionSegmentation(matlabbatch, BIDS, opt, subLabel) + % + % 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) + % + % (C) Copyright 2021 CPP_SPM developers + + printBatchName('Lesion segmentation'); + + % find anatomical file + [anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt); + unified_segmentation.step1data{1} = fullfile(anatDataDir, anatImage); + + % 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(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 diff --git a/src/batches/setBatchMeanAnatAndMask.m b/src/batches/setBatchMeanAnatAndMask.m index bd845192..cb661973 100644 --- a/src/batches/setBatchMeanAnatAndMask.m +++ b/src/batches/setBatchMeanAnatAndMask.m @@ -1,8 +1,6 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - 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:: % @@ -18,43 +16,39 @@ % % :returns: - :matlabbatch: (structure) % + % (C) Copyright 2019 CPP_SPM developers - [group, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); printBatchName('create mean anatomical image and mask'); inputAnat = {}; inputMask = {}; - for iGroup = 1:length(group) - - groupName = group(iGroup).name; - - for iSub = 1:group(iGroup).numSub + for iSub = 1:numel(opt.subjects) - subID = group(iGroup).subNumber{iSub}; + subLabel = opt.subjects{iSub}; - printProcessingSubject(groupName, iSub, subID); + printProcessingSubject(iSub, subLabel); - %% Anat - [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, 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(subID, 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 end %% Generate the equation to get the mean of the mask and structural image diff --git a/src/batches/setBatchNormalizationSpatialPrepro.m b/src/batches/setBatchNormalizationSpatialPrepro.m index c34f7cd5..918ae121 100644 --- a/src/batches/setBatchNormalizationSpatialPrepro.m +++ b/src/batches/setBatchNormalizationSpatialPrepro.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function matlabbatch = setBatchNormalizationSpatialPrepro(matlabbatch, opt, voxDim) % % Short description of what the function does goes here. @@ -17,10 +15,11 @@ % % :returns: - :matlabbatch: (structure) % + % (C) Copyright 2019 CPP_SPM developers jobsToAdd = numel(matlabbatch) + 1; - for iJob = jobsToAdd:(jobsToAdd + 4) + for iJob = jobsToAdd:(jobsToAdd + 5) % set the deformation field for all the images we are about to normalize deformationField = ... @@ -96,4 +95,16 @@ '.', 'tiss', '()', {3}, ... '.', 'c', '()', {':'})); + % NORMALIZE SKULSTRIPPED STRUCTURAL + printBatchName('normalise skullstripped anatomical images'); + matlabbatch{jobsToAdd + 5}.spm.spatial.normalise.write.subj.resample(1) = ... + cfg_dep('Image Calculator: skullstripped anatomical', ... + substruct( ... + '.', 'val', '{}', {opt.orderBatches.skullStripping}, ... + '.', 'val', '{}', {1}, ... + '.', 'val', '{}', {1}), ... + substruct('.', 'files')); + % size 3 allow to run RunQA / original voxel size at acquisition + matlabbatch{jobsToAdd + 5}.spm.spatial.normalise.write.woptions.vox = [1 1 1]; + end diff --git a/src/batches/setBatchNormalize.m b/src/batches/setBatchNormalize.m index 4416d37d..5af36e79 100644 --- a/src/batches/setBatchNormalize.m +++ b/src/batches/setBatchNormalize.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function matlabbatch = setBatchNormalize(matlabbatch, deformField, voxDim, imgToResample) % % Short description of what the function does goes here. @@ -19,6 +17,7 @@ % % :returns: - :matlabbatch: (structure) % + % (C) Copyright 2019 CPP_SPM developers if nargin > 1 && ~isempty(deformField) matlabbatch{end + 1}.spm.spatial.normalise.write.subj.def(1) = deformField; diff --git a/src/batches/setBatchPrintFigure.m b/src/batches/setBatchPrintFigure.m index d7d5d3e7..2f6fb363 100644 --- a/src/batches/setBatchPrintFigure.m +++ b/src/batches/setBatchPrintFigure.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function matlabbatch = setBatchPrintFigure(matlabbatch, figureName) % % template to creae new setBatch functions @@ -14,8 +12,10 @@ % :type figureName: string % % :returns: - :matlabbatch: (structure) The matlabbatch ready to run the spm job + % + % (C) Copyright 2020 CPP_SPM developers - if ~spm('CmdLine', true) + if spm('CmdLine', true) printBatchName('print figure'); diff --git a/src/batches/setBatchRealign.m b/src/batches/setBatchRealign.m index e509008e..f04d6e63 100644 --- a/src/batches/setBatchRealign.m +++ b/src/batches/setBatchRealign.m @@ -1,12 +1,14 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function [matlabbatch, voxDim] = setBatchRealign(varargin) % % Set the batch for realign / realign and reslice / realign and unwarp % % 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,20 +18,23 @@ % :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) - - % TODO: - % make which image is resliced more consistent 'which = []' + % + % TODO:: + % + % make which image is resliced more consistent 'which = []' + % + % (C) Copyright 2020 CPP_SPM developers 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 +66,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/setBatchReslice.m b/src/batches/setBatchReslice.m index cda6fa0b..534a4f92 100644 --- a/src/batches/setBatchReslice.m +++ b/src/batches/setBatchReslice.m @@ -1,44 +1,41 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - 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 % + % (C) Copyright 2020 CPP_SPM developers printBatchName('reslicing'); 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) + write.source = {sourceImages}; + elseif iscell(sourceImages) + write.source = sourceImages; end + matlabbatch{end + 1}.spm.spatial.coreg.write = write; + end diff --git a/src/batches/setBatchResults.m b/src/batches/setBatchResults.m index 364315a6..4e4cf24b 100644 --- a/src/batches/setBatchResults.m +++ b/src/batches/setBatchResults.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function matlabbatch = setBatchResults(matlabbatch, result) % % Outputs the typical matlabbatch to compute the results for a given contrast @@ -21,63 +19,81 @@ % :returns: - :matlabbatch: (structure) % % + % (C) Copyright 2019 CPP_SPM developers + + 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 = 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')}; + stats.results.spmmat = {fullfile(result.dir, 'SPM.mat')}; + + stats.results.conspec.titlestr = returnName(result); - matlabbatch{end}.spm.stats.results.conspec.titlestr = returnName(result); + 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.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.units = 1; - matlabbatch{end}.spm.stats.results.units = 1; + matlabbatch{end + 1}.spm.stats = stats; - matlabbatch{end}.spm.stats.results.export = []; + %% 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; - matlabbatch{end}.spm.stats.results.export{end}.nidm.group.label = result.label; + 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/setBatchSTC.m b/src/batches/setBatchSTC.m index 473687dd..462493a0 100644 --- a/src/batches/setBatchSTC.m +++ b/src/batches/setBatchSTC.m @@ -1,6 +1,4 @@ -% (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 % @@ -30,6 +28,7 @@ % If not specified this function will take the mid-volume time point as reference % to do the slice timing correction % + % (C) Copyright 2019 CPP_SPM developers % get slice order sliceOrder = getSliceOrder(opt, 1); @@ -49,6 +48,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); @@ -83,21 +86,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..2af36499 100644 --- a/src/batches/setBatchSaveCoregistrationMatrix.m +++ b/src/batches/setBatchSaveCoregistrationMatrix.m @@ -1,6 +1,4 @@ -% (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,21 +12,22 @@ % :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: % + % (C) Copyright 2020 CPP_SPM developers printBatchName('saving coregistration matrix'); % 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/setBatchSegmentation.m b/src/batches/setBatchSegmentation.m index 395cc588..2b86fe4b 100644 --- a/src/batches/setBatchSegmentation.m +++ b/src/batches/setBatchSegmentation.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function matlabbatch = setBatchSegmentation(matlabbatch, opt, imageToSegment) % % Creates a batch to segment the anatomical image @@ -16,6 +14,7 @@ % % :returns: :matlabbatch: (structure) % + % (C) Copyright 2020 CPP_SPM developers printBatchName('Segmentation anatomical image'); diff --git a/src/batches/setBatchSelectAnat.m b/src/batches/setBatchSelectAnat.m index cb60668f..b934ce5f 100644 --- a/src/batches/setBatchSelectAnat.m +++ b/src/batches/setBatchSelectAnat.m @@ -1,6 +1,4 @@ -% (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 +13,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) % @@ -26,10 +24,12 @@ % - session to select the anat from = opt.anatReference.session (default = 1) % % We assume that the first anat of that type is the "correct" one + % + % (C) Copyright 2020 CPP_SPM developers 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..db416ea3 100644 --- a/src/batches/setBatchSkullStripping.m +++ b/src/batches/setBatchSkullStripping.m @@ -1,13 +1,11 @@ -% (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 +14,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 % @@ -33,10 +31,12 @@ % Any voxel with p(grayMatter) + p(whiteMatter) + p(CSF) > threshold % will be included in the skull stripping mask. % + % + % (C) Copyright 2020 CPP_SPM developers 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/setBatchSmoothConImages.m b/src/batches/setBatchSmoothConImages.m index 8e19b61c..f92b5a06 100644 --- a/src/batches/setBatchSmoothConImages.m +++ b/src/batches/setBatchSmoothConImages.m @@ -1,6 +1,4 @@ -% (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 % @@ -21,32 +19,26 @@ % % :returns: - :matlabbatch: % + % (C) Copyright 2019 CPP_SPM developers printBatchName('smoothing contrast images'); - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) - - groupName = group(iGroup).name; - - for iSub = 1:group(iGroup).numSub - - subNumber = group(iGroup).subNumber{iSub}; + for iSub = 1:numel(opt.subjects) - printProcessingSubject(groupName, iSub, subNumber); + subLabel = opt.subjects{iSub}; - ffxDir = getFFXdir(subNumber, funcFWHM, opt); + printProcessingSubject(iSub, subLabel); - conImg = spm_select('FPlist', ffxDir, '^con*.*nii$'); - data = cellstr(conImg); + ffxDir = getFFXdir(subLabel, funcFWHM, opt); - matlabbatch = setBatchSmoothing( ... - matlabbatch, ... - data, ... - conFWHM, ... - [spm_get_defaults('smooth.prefix'), num2str(conFWHM)]); + conImg = spm_select('FPlist', ffxDir, '^con*.*nii$'); + data = cellstr(conImg); - end + matlabbatch = setBatchSmoothing( ... + matlabbatch, ... + data, ... + conFWHM, ... + [spm_get_defaults('smooth.prefix'), num2str(conFWHM)]); end diff --git a/src/batches/setBatchSmoothing.m b/src/batches/setBatchSmoothing.m index f0e353de..349077bc 100644 --- a/src/batches/setBatchSmoothing.m +++ b/src/batches/setBatchSmoothing.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function matlabbatch = setBatchSmoothing(matlabbatch, images, FWHM, prefix) % % Small wrapper to create smoothing batch @@ -20,6 +18,7 @@ % :returns: - :matlabbatch: (structure) % % + % (C) Copyright 2019 CPP_SPM developers printBatchName('smoothing images'); diff --git a/src/batches/setBatchSmoothingFunc.m b/src/batches/setBatchSmoothingFunc.m index 3baf0487..e2aea02c 100644 --- a/src/batches/setBatchSmoothingFunc.m +++ b/src/batches/setBatchSmoothingFunc.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function matlabbatch = setBatchSmoothingFunc(matlabbatch, BIDS, opt, subID, funcFWHM) % % Short description of what the function does goes here. @@ -22,6 +20,7 @@ % :returns: - :matlabbatch: (structure) % % + % (C) Copyright 2019 CPP_SPM developers printBatchName('smoothing functional images'); diff --git a/src/batches/setBatchSubjectLevelContrasts.m b/src/batches/setBatchSubjectLevelContrasts.m index ad971576..7791b17c 100644 --- a/src/batches/setBatchSubjectLevelContrasts.m +++ b/src/batches/setBatchSubjectLevelContrasts.m @@ -1,28 +1,27 @@ -% (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: % % :returns: - :matlabbatch: % + % (C) Copyright 2019 CPP_SPM developers 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..70fd2795 100644 --- a/src/batches/setBatchSubjectLevelGLMSpec.m +++ b/src/batches/setBatchSubjectLevelGLMSpec.m @@ -1,12 +1,10 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function matlabbatch = setBatchSubjectLevelGLMSpec(varargin) % % Short description of what the function does goes here. % % 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. @@ -18,8 +16,9 @@ % :returns: - :argout1: (type) (dimension) % - :argout2: (type) (dimension) % + % (C) Copyright 2019 CPP_SPM developers - [matlabbatch, BIDS, opt, subID, funcFWHM] = deal(varargin{:}); + [matlabbatch, BIDS, opt, subLabel, funcFWHM] = deal(varargin{:}); printBatchName('specify subject level fmri model'); @@ -34,7 +33,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 +63,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 +86,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 +107,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..7a73cd55 100644 --- a/src/batches/setBatchSubjectLevelResults.m +++ b/src/batches/setBatchSubjectLevelResults.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function matlabbatch = setBatchSubjectLevelResults(varargin) % % Short description of what the function does goes here. @@ -12,8 +10,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: @@ -23,8 +21,9 @@ % % :returns: - :matlabbatch: (structure) % + % (C) Copyright 2019 CPP_SPM developers - [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,12 +32,24 @@ 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); + result.outputNameStructure = struct( ... + 'type', 'spmT', ... + 'ext', '.nii', ... + 'sub', '', ... + 'task', opt.taskName, ... + 'space', opt.space, ... + 'desc', '', ... + 'label', 'XXXX', ... + 'p', '', ... + 'k', '', ... + 'MC', ''); + matlabbatch = setBatchResults(matlabbatch, result); end diff --git a/src/defaults/checkOptions.m b/src/defaults/checkOptions.m index 12f060e9..878184df 100644 --- a/src/defaults/checkOptions.m +++ b/src/defaults/checkOptions.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function opt = checkOptions(opt) % % Check the option inputs and add any missing field with some defaults @@ -19,10 +17,12 @@ % - :opt: the option structure with missing values filled in by the defaults. % % REQUIRED FIELDS: + % % - ``opt.taskName`` % - ``opt.dataDir`` % % IMPORTANT OPTIONS (with their defaults): + % % - ``opt.groups = {''}`` - group of subjects to analyze % - ``opt.subjects = {[]}`` - suject to run in each group % space where we conduct the analysis @@ -37,25 +37,40 @@ % 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 = 1`` - session number 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 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 + % - ``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.glm.roibased.do`` + % - ``opt.glm.QA.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 + % `_ for more info. + % + % + % + % (C) Copyright 2019 CPP_SPM developers fieldsToSet = setDefaultOption(); - opt = setDefaultFields(opt, fieldsToSet); + opt = setFields(opt, fieldsToSet); checkFields(opt); @@ -65,6 +80,8 @@ opt = orderfields(opt); + opt = setStatsDir(opt); + end function fieldsToSet = setDefaultOption() @@ -72,13 +89,17 @@ fieldsToSet.dataDir = ''; fieldsToSet.derivativesDir = ''; + fieldsToSet.dir = struct('raw', '', ... + 'derivatives', ''); fieldsToSet.groups = {''}; fieldsToSet.subjects = {[]}; fieldsToSet.zeropad = 2; + fieldsToSet.query = struct([]); + fieldsToSet.anatReference.type = 'T1w'; - fieldsToSet.anatReference.session = []; + fieldsToSet.anatReference.session = ''; %% Options for slice time correction % all in seconds @@ -101,6 +122,9 @@ fieldsToSet.model.hrfDerivatives = [0 0]; fieldsToSet.contrastList = {}; + fieldsToSet.glm.QA.do = true; + fieldsToSet.glm.roibased.do = false; + % specify the results to compute fieldsToSet.result.Steps = returnDefaultResultsStructure(); @@ -112,7 +136,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( ... @@ -123,8 +147,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.'); @@ -132,6 +154,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/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/defaults/createDefaultModel.m b/src/defaults/createDefaultModel.m index 0de2252f..9a541612 100644 --- a/src/defaults/createDefaultModel.m +++ b/src/defaults/createDefaultModel.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function opt = createDefaultModel(BIDS, opt) % % Creates a default model json file. @@ -49,6 +47,7 @@ % % createDefaultModel(BIDS, opt); % + % (C) Copyright 2020 CPP_SPM developers % TODO deal with the Transformations and Convolve fields diff --git a/src/defaults/datasetDescriptionDefaults.m b/src/defaults/datasetDescriptionDefaults.m index a9bc50bf..86d51462 100644 --- a/src/defaults/datasetDescriptionDefaults.m +++ b/src/defaults/datasetDescriptionDefaults.m @@ -1,6 +1,7 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function datasetDescription = datasetDescriptionDefaults() + % + % + % (C) Copyright 2020 CPP_SPM developers datasetDescription.Name = 'cpp_spm outputs'; datasetDescription.BIDSVersion = '1.4.1'; diff --git a/src/defaults/miss_hit.cfg b/src/defaults/miss_hit.cfg index 636a9186..a9f91c98 100644 --- a/src/defaults/miss_hit.cfg +++ b/src/defaults/miss_hit.cfg @@ -1,10 +1 @@ -# styly guide (https://florianschanda.github.io/miss_hit/style_checker.html) -line_length: 100 regex_function_name: "[a-z]+(_*([a-zA-Z0-9]){1}[A-Za-z]+)*" # almost anything goes in the root folder -copyright_entity: "CPP BIDS SPM-pipeline developpers" - -# metrics limit for the code quality (https://florianschanda.github.io/miss_hit/metrics.html) -metric "cnest": limit 4 -metric "file_length": limit 400 -metric "cyc": limit 12 -metric "parameters": limit 6 \ No newline at end of file diff --git a/src/defaults/returnDefaultResultsStructure.m b/src/defaults/returnDefaultResultsStructure.m index 96cbeb08..7d1eaa21 100644 --- a/src/defaults/returnDefaultResultsStructure.m +++ b/src/defaults/returnDefaultResultsStructure.m @@ -1,6 +1,6 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function results = returnDefaultResultsStructure() + % + % (C) Copyright 2019 CPP_SPM developers Contrasts = struct( ... 'Name', '', ... diff --git a/src/defaults/returnEmptyModel.m b/src/defaults/returnEmptyModel.m index 3148a97f..d4664741 100644 --- a/src/defaults/returnEmptyModel.m +++ b/src/defaults/returnEmptyModel.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function content = returnEmptyModel() % % Creates the content of a basic model.json file for GLM analysis with @@ -22,6 +20,7 @@ % filename = fullfile(pwd, 'models', 'model-empty_smdl.json') % spm_jsonwrite(filename, content, jsonOptions); % + % (C) Copyright 2020 CPP_SPM developers content.Name = ' '; content.Description = ' '; diff --git a/src/defaults/spm_my_defaults.m b/src/defaults/spm_my_defaults.m index 43b67496..eac7591e 100644 --- a/src/defaults/spm_my_defaults.m +++ b/src/defaults/spm_my_defaults.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function spm_my_defaults() % % Short description of what the function does goes here. @@ -15,6 +13,8 @@ function spm_my_defaults() % When "not enough" information is specified in the batch files, SPM falls % back on the defaults to fill in the blanks. This allows to make the % script simpler. + % + % (C) Copyright 2019 CPP_SPM developers global defaults diff --git a/src/fieldmaps/getBlipDirection.m b/src/fieldmaps/getBlipDirection.m index 33bbb2db..1d8660db 100644 --- a/src/fieldmaps/getBlipDirection.m +++ b/src/fieldmaps/getBlipDirection.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function blipDir = getBlipDirection(metadata) % % Gets the total read out time of a sequence. @@ -15,6 +13,7 @@ % % Used to create the voxel dsiplacement map (VDM) from the fieldmap % + % (C) Copyright 2020 CPP_SPM developers blipDir = 1; diff --git a/src/fieldmaps/getMetadataFromIntendedForFunc.m b/src/fieldmaps/getMetadataFromIntendedForFunc.m index 29d8ee57..1bfeb148 100644 --- a/src/fieldmaps/getMetadataFromIntendedForFunc.m +++ b/src/fieldmaps/getMetadataFromIntendedForFunc.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function [totalReadoutTime, blipDir] = getMetadataFromIntendedForFunc(BIDS, fmapMetadata) % % Gets metadata of the associated bold file: @@ -26,6 +24,8 @@ % % - if there are several func file for this fmap and they have different % characteristic this may require creating a VDM for each + % + % (C) Copyright 2020 CPP_SPM developers for iFile = 1:size(fmapMetadata.IntendedFor) diff --git a/src/fieldmaps/getTotalReadoutTime.m b/src/fieldmaps/getTotalReadoutTime.m index f9f2122a..46c5c5c1 100644 --- a/src/fieldmaps/getTotalReadoutTime.m +++ b/src/fieldmaps/getTotalReadoutTime.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function totalReadoutTime = getTotalReadoutTime(metadata) % % Gets the total read out time of a sequence. @@ -15,6 +13,7 @@ % % Used to create the voxel dsiplacement map (VDM) from the fieldmap % + % (C) Copyright 2020 CPP_SPM developers totalReadoutTime = ''; diff --git a/src/fieldmaps/getVdmFile.m b/src/fieldmaps/getVdmFile.m index 2c96416e..51bb36c0 100644 --- a/src/fieldmaps/getVdmFile.m +++ b/src/fieldmaps/getVdmFile.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function vdmFile = getVdmFile(BIDS, opt, boldFilename) % % returns the voxel displacement map associated with a given bold file @@ -17,6 +15,7 @@ % % :returns: - :vdmFile: (string) % + % (C) Copyright 2020 CPP_SPM developers vdmFile = ''; diff --git a/src/getAnatFilename.m b/src/getAnatFilename.m index 74b83485..f0b2dd37 100644 --- a/src/getAnatFilename.m +++ b/src/getAnatFilename.m @@ -1,61 +1,111 @@ -% (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. + % 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, subID, 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. + % (C) Copyright 2020 CPP_SPM developers - anatType = opt.anatReference.type; + anatSuffix = opt.anatReference.type; + anatSession = opt.anatReference.session; - sessions = getInfo(BIDS, subID, 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', subID, ... - 'type', anatType); - if ~isempty(opt.anatReference.session) - anatSession = opt.anatReference.session; - anat = bids.query(BIDS, 'data', ... - 'sub', subID, ... - 'ses', sessions{anatSession}, ... - 'type', anatType); - end + 'sub', subLabel, ... + 'ses', anatSession, ... + 'type', anatSuffix); if isempty(anat) - anat = bids.query(BIDS, 'data', ... - 'sub', subID, ... - 'type', anatType); - error('No anat file for the subject %s. Here are all anat file:\n%s', ... - subID, ... - 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/getBoldFilename.m b/src/getBoldFilename.m index 6c5f5342..243e6495 100644 --- a/src/getBoldFilename.m +++ b/src/getBoldFilename.m @@ -1,31 +1,32 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - 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. + % + % (C) Copyright 2020 CPP_SPM developers [BIDS, subID, sessionID, runID, opt] = deal(varargin{:}); diff --git a/src/getData.m b/src/getData.m index d28da2b2..fd60aec6 100644 --- a/src/getData.m +++ b/src/getData.m @@ -1,12 +1,10 @@ -% (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,47 +15,18 @@ % 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. % % IMPORTANT NOTE: if you specify the type variable for T1w then you must % make sure that the T1w.json is also present in the anat folder because % of the way the bids.query function works at the moment + % + % (C) Copyright 2020 CPP_SPM developers if nargin < 2 || (exist('BIDSdir', 'var') && isempty(BIDSdir)) % The directory where the derivatives are located @@ -70,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'); @@ -88,43 +61,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> + opt = getMetaData(BIDS, opt, opt.subjects, type); - group = getSpecificSubjects(opt, group, iGroup, subjects); - - % 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/getFuncVoxelDims.m b/src/getFuncVoxelDims.m index 99c825b2..bb38ab19 100644 --- a/src/getFuncVoxelDims.m +++ b/src/getFuncVoxelDims.m @@ -1,26 +1,26 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function [voxDim, opt] = getFuncVoxelDims(opt, subFuncDataDir, prefix, fileName) % % Short description of what the function does goes here. % % 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) % % + % (C) Copyright 2020 CPP_SPM developers % get native resolution to reuse it at normalisation; if ~isempty(opt.funcVoxelDims) % If voxel dimensions is defined in the opt diff --git a/src/getInfo.m b/src/getInfo.m index 8e56ef5a..c2ca3aed 100644 --- a/src/getInfo.m +++ b/src/getInfo.m @@ -1,38 +1,51 @@ -% (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) + % + % If info = ``sessions``, this returns name of the sessions and their number:: + % + % [sessions, nbSessions] = getInfo(BIDS, subID, opt, 'sessions') + % + % If info = ``runs``, this returns name of the runs and their number for a + % specified session:: + % + % [runs, nbRuns] = getInfo(BIDS, subLabel, opt, 'runs', sessionID) + % + % If info = ``filename``, this returns the name of the file for a 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 BIDS: (structure) returned by bids.query when exploring a BIDS data set. - % :param subID: ID of the subject - % :param opt: (structure) Mostly used to find the task name. - % :param info: (strint) ``sessions``, ``runs``, ``filename``. - % :param varargin: see below + % :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 % - % - 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`` - % - 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`` + % :param opt: Used to find the task name and to pass extra ``query`` + % options. + % :type opt: structure % - % for a given BIDS data set, subject identity, and info type, + % :param info: ``sessions``, ``runs``, ``filename``. + % :type info: string % - % if info = Sessions, this returns name of the sessions and their number + % :param sessionLabel: session label (for `ses-001`, the label will be `001`) + % :type sessionLabel: string % - % if info = Runs, this returns name of the runs and their number for an specified session. + % :param runIdx: run index label (for `run-001`, the label will be `001`) + % :type runIdx: string % - % if info = Filename, this returns the name of the file for an specified - % session and run. + % :param type: datatype (``bold``, ``events``, ``physio``) + % :type type: string % + % (C) Copyright 2020 CPP_SPM developers varargout = {}; %#ok<*NASGU> @@ -40,9 +53,21 @@ case 'sessions' - sessions = bids.query(BIDS, 'sessions', ... - 'sub', subID, ... - '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 + overwrite = true; + query = setFields(query, opt.query, overwrite); + + sessions = bids.query(BIDS, 'sessions', query); + nbSessions = size(sessions, 2); if nbSessions == 0 nbSessions = 1; @@ -55,12 +80,17 @@ session = varargin{1}; - runs = bids.query(BIDS, 'runs', ... - 'sub', subID, ... - 'task', opt.taskName, ... - 'ses', session, ... - 'type', 'bold'); - nbRuns = size(runs, 2); % Get the number of runs + query = struct( ... + 'sub', subLabel, ... + 'task', opt.taskName, ... + 'ses', session, ... + 'type', 'bold'); + + query = setFields(query, opt.query); + + runs = bids.query(BIDS, 'runs', query); + + nbRuns = size(runs, 2); if nbRuns == 0 nbRuns = 1; @@ -73,12 +103,19 @@ [session, run, type] = deal(varargin{:}); - varargout = bids.query(BIDS, 'data', ... - 'sub', subID, ... - 'run', run, ... - 'ses', session, ... - 'task', opt.taskName, ... - 'type', type); + query = struct( ... + 'sub', subLabel, ... + 'task', opt.taskName, ... + 'ses', session, ... + 'run', run, ... + 'type', type); + + % 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/getMeanFuncFilename.m b/src/getMeanFuncFilename.m index 98297502..bea6ea44 100644 --- a/src/getMeanFuncFilename.m +++ b/src/getMeanFuncFilename.m @@ -1,36 +1,28 @@ -% (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) - % - % [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt) + % :returns: - :meanImage: (string) + % - :meanFuncDir: (string) % - % Get the filename and the directory of an anat file for a given session / - % run. - % Unzips the file if necessary. + % (C) Copyright 2020 CPP_SPM developers - 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/getPrefix.m b/src/getPrefix.m index e99d41cc..ef334d9b 100644 --- a/src/getPrefix.m +++ b/src/getPrefix.m @@ -1,26 +1,23 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - 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 + % (C) Copyright 2020 CPP_SPM developers if nargin < 3 funcFWHM = 0; diff --git a/src/getRealignParamFile.m b/src/getRealignParamFile.m index d5cc6111..e049ce9b 100644 --- a/src/getRealignParamFile.m +++ b/src/getRealignParamFile.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function realignParamFile = getRealignParamFile(fullpathBoldFileName, prefix) % % Short description of what the function does goes here. @@ -18,6 +16,8 @@ % % :returns: - :argout1: (type) (dimension) % - :argout2: (type) (dimension) + % + % (C) Copyright 2020 CPP_SPM developers [funcDataDir, boldFileName] = spm_fileparts(fullpathBoldFileName); diff --git a/src/getSliceOrder.m b/src/getSliceOrder.m index 8cc2f553..1d2e1037 100644 --- a/src/getSliceOrder.m +++ b/src/getSliceOrder.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function sliceOrder = getSliceOrder(opt, verbose) % % Get the slice order information from the BIDS metadata or from the ``opt`` @@ -25,6 +23,7 @@ % % See also: ``bidsSTC`` % + % (C) Copyright 2020 CPP_SPM developers if nargin < 2 verbose = false; 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/group_level/getGrpLevelContrastToCompute.m b/src/group_level/getGrpLevelContrastToCompute.m index 52ec91ad..d7bedf6d 100644 --- a/src/group_level/getGrpLevelContrastToCompute.m +++ b/src/group_level/getGrpLevelContrastToCompute.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function [grpLvlCon, iStep] = getGrpLevelContrastToCompute(opt) % % Returns the autocontrast part of the dataset step of the BIDS model @@ -14,6 +12,7 @@ % :returns: - :grpLvlCon: % - :iStep: % + % (C) Copyright 2019 CPP_SPM developers model = spm_jsonread(opt.model.file); diff --git a/src/group_level/getRFXdir.m b/src/group_level/getRFXdir.m index 69e0ff51..a54e58b9 100644 --- a/src/group_level/getRFXdir.m +++ b/src/group_level/getRFXdir.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function rfxDir = getRFXdir(opt, funcFWHM, conFWHM) % % Sets the name the group level analysis directory and creates it if it does not exist @@ -21,15 +19,22 @@ % % :returns: :rfxDir: (string) Fullpath of the group level directory % + % (C) Copyright 2019 CPP_SPM developers + + 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-', convertToValidCamelCase(model.Name)]; + end rfxDir = fullfile( ... - opt.derivativesDir, ... + opt.dir.stats, ... 'group', ... - ['rfx_task-', opt.taskName], ... - ['rfx_funcFWHM-', num2str(funcFWHM), '_conFWHM-', num2str(conFWHM)]); + glmDirName); - if ~exist(rfxDir, 'dir') - mkdir(rfxDir); - end + spm_mkdir(rfxDir); end diff --git a/src/reports/copyFigures.m b/src/reports/copyFigures.m index 607aecd1..6a939581 100644 --- a/src/reports/copyFigures.m +++ b/src/reports/copyFigures.m @@ -1,29 +1,28 @@ -% (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 % % + % (C) Copyright 2019 CPP_SPM developers - 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 +31,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 diff --git a/src/reports/copyGraphWindownOutput.m b/src/reports/copyGraphWindownOutput.m index a7821cd0..825043db 100644 --- a/src/reports/copyGraphWindownOutput.m +++ b/src/reports/copyGraphWindownOutput.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function imgNb = copyGraphWindownOutput(opt, subID, action, imgNb) % % Looks into the current directory for an ``spm_.*imgNb.png`` file and moves it into @@ -23,6 +21,7 @@ % % :returns: :imgNb: (integer) number of the next image to get. % + % (C) Copyright 2019 CPP_SPM developers if nargin < 4 || isempty(imgNb) imgNb = 1; diff --git a/src/reports/reportBIDS.m b/src/reports/reportBIDS.m index 4c617840..e0943d19 100644 --- a/src/reports/reportBIDS.m +++ b/src/reports/reportBIDS.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function reportBIDS(opt) % % Prints out a human readable description of a BIDS data set. @@ -16,6 +14,8 @@ function reportBIDS(opt) % % - save output in the derivatires folder % derivativeDir = fullfile(rawDir, '..', 'derivatives', 'cpp_spm'); + % + % (C) Copyright 2020 CPP_SPM developers bids.report(opt.dataDir); diff --git a/src/results/returnName.m b/src/results/returnName.m index 8fcc2ffb..12fb43d1 100644 --- a/src/results/returnName.m +++ b/src/results/returnName.m @@ -1,6 +1,8 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function name = returnName(result) + % + % To help naming of files generated when computing results of a given contrast + % + % (C) Copyright 2019 CPP_SPM developers name = sprintf('%s_p-%0.3f_k-%i_MC-%s', ... result.Contrasts.Name, ... diff --git a/src/results/setMontage.m b/src/results/setMontage.m index 23e949d1..0507d67f 100644 --- a/src/results/setMontage.m +++ b/src/results/setMontage.m @@ -1,11 +1,11 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function montage = setMontage(result) - + % % TO DO % - adapt so that the background image is in MNI only if opt.space is MNI % - add possibility to easily select mean functional or the anatomical: % - at the group level or subject level + % + % (C) Copyright 2019 CPP_SPM developers montage.background = {result.Output.montage.background}; montage.orientation = result.Output.montage.orientation; diff --git a/src/setDerivativesDir.m b/src/setDerivativesDir.m index 04c9c37a..7b27b581 100644 --- a/src/setDerivativesDir.m +++ b/src/setDerivativesDir.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function opt = setDerivativesDir(opt) % % Sets the derivatives folder and the directory where to save the SPM jobs. @@ -17,34 +15,36 @@ % - :opt: structure or json filename containing the options. See % ``checkOptions()`` and ``loadAndCheckOptions()``. % - % Examples: - % % opt.dataDir = '/home/remi/data'; - % % opt.taskName = 'testTask'; - % % opt = setDerivativesDir(opt); - % % - % % disp(opt.derivativesDir) - % %|| '/home/remi/data/../derivatives/cpp_spm' - % % - % % disp(opt.opt.jobsDir) - % %|| '/home/remi/data/../derivatives/cpp_spm/JOBS/testTask + % Examples:: + % + % opt.dataDir = '/home/remi/data'; + % opt.taskName = 'testTask'; + % opt = setDerivativesDir(opt); + % + % disp(opt.derivativesDir) + % '/home/remi/data/../derivatives/cpp_spm' + % + % disp(opt.opt.jobsDir) + % '/home/remi/data/../derivatives/cpp_spm/JOBS/testTask % - % % opt.dataDir = '/home/remi/data'; - % % opt.dataDir = '/home/remi/otherFolder'; - % % opt.taskName = 'testTask'; - % % opt = setDerivativesDir(opt); - % % - % % disp(opt.derivativesDir) - % %|| '/home/remi/otherFolder/derivatives/cpp_spm' + % opt.dataDir = '/home/remi/data'; + % opt.dataDir = '/home/remi/otherFolder'; + % opt.taskName = 'testTask'; + % opt = setDerivativesDir(opt); % - % % opt.dataDir = '/home/remi/data'; - % % opt.dataDir = '/home/remi/derivatives/preprocessing'; - % % opt.taskName = 'testTask'; - % % opt = setDerivativesDir(opt); - % % - % % disp(opt.derivativesDir) - % %|| '/home/remi/otherFolder/derivatives/preprocessing' + % disp(opt.derivativesDir) + % '/home/remi/otherFolder/derivatives/cpp_spm' % + % opt.dataDir = '/home/remi/data'; + % opt.dataDir = '/home/remi/derivatives/preprocessing'; + % opt.taskName = 'testTask'; + % opt = setDerivativesDir(opt); % + % disp(opt.derivativesDir) + % '/home/remi/otherFolder/derivatives/preprocessing' + % + % + % (C) Copyright 2020 CPP_SPM developers if ~isfield(opt, 'derivativesDir') || isempty(opt.derivativesDir) opt.derivativesDir = fullfile(opt.dataDir, '..', 'derivatives', 'cpp_spm'); @@ -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/setStatsDir.m b/src/setStatsDir.m new file mode 100644 index 00000000..637f1420 --- /dev/null +++ b/src/setStatsDir.m @@ -0,0 +1,17 @@ +function opt = setStatsDir(opt) + % + % USAGE:: + % + % opt = setStatsDir(opt) + % + % (C) Copyright 2021 CPP_SPM developers + + opt = setDerivativesDir(opt); + + if ~isfield(opt.dir, 'stats') + opt.dir.stats = fullfile(opt.derivativesDir, '..', 'cpp_spm-stats'); + end + + opt.dir.stats = spm_file(opt.dir.stats, 'cpath'); + +end diff --git a/src/subject_level/convertOnsetTsvToMat.m b/src/subject_level/convertOnsetTsvToMat.m index c81cc80f..3076428e 100644 --- a/src/subject_level/convertOnsetTsvToMat.m +++ b/src/subject_level/convertOnsetTsvToMat.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function fullpathOnsetFileName = convertOnsetTsvToMat(opt, tsvFile) % % Converts an events.tsv file to an onset file suitable for SPM subject level @@ -16,8 +14,9 @@ % :param tsvFile: % :type tsvFile: string % - % :returns: - :fullpathOnsetFileName: (string) name of the output `.mat` file. + % :returns: :fullpathOnsetFileName: (string) name of the output ``.mat`` file. % + % (C) Copyright 2019 CPP_SPM developers % [pth, file, ext] = spm_fileparts(tsvFile); @@ -89,7 +88,12 @@ % save the onsets as a matfile [pth, file] = spm_fileparts(tsvFile); - fullpathOnsetFileName = fullfile(pth, ['onsets_' file '.mat']); + p = bids.internal.parse_filename(file); + p.space = opt.space; + p.type = 'onsets'; + p.ext = '.mat'; + + fullpathOnsetFileName = fullfile(pth, createFilename(p)); save(fullpathOnsetFileName, ... 'names', 'onsets', 'durations', ... diff --git a/src/subject_level/createAndReturnOnsetFile.m b/src/subject_level/createAndReturnOnsetFile.m index 73e50eed..92a22ad6 100644 --- a/src/subject_level/createAndReturnOnsetFile.m +++ b/src/subject_level/createAndReturnOnsetFile.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function onsetFileName = createAndReturnOnsetFile(opt, subID, tsvFile, funcFWHM) % % For a given events.tsv file it creates a .mat file that can directly be used @@ -20,9 +18,10 @@ % GLM. Necessary for the GLM directory. % :type funcFWHM: float % - % :returns: - :onsetFileName: (string) fullpath name of the file created. - % Removes any prefix. + % :returns: :onsetFileName: (string) fullpath name of the file created. + % % + % (C) Copyright 2019 CPP_SPM developers onsetFileName = convertOnsetTsvToMat(opt, tsvFile); diff --git a/src/subject_level/deleteResidualImages.m b/src/subject_level/deleteResidualImages.m index a6b10bf4..59838998 100644 --- a/src/subject_level/deleteResidualImages.m +++ b/src/subject_level/deleteResidualImages.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function deleteResidualImages(ffxDir) % % USAGE:: @@ -9,6 +7,7 @@ function deleteResidualImages(ffxDir) % :param ffxDir: % :type ffxDir: string % + % (C) Copyright 2020 CPP_SPM developers delete(fullfile(ffxDir, 'Res_*.nii')); delete(fullfile(ffxDir, 'res4d.nii*')); diff --git a/src/subject_level/getBoldFilenameForFFX.m b/src/subject_level/getBoldFilenameForFFX.m index 117ed609..b53b6f3e 100644 --- a/src/subject_level/getBoldFilenameForFFX.m +++ b/src/subject_level/getBoldFilenameForFFX.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function [boldFileName, prefix] = getBoldFilenameForFFX(varargin) % % Gets the filename for this bold run for this task for the FFX setup @@ -26,6 +24,7 @@ % - :prefix: (srting) % % + % (C) Copyright 2020 CPP_SPM developers [BIDS, opt, subID, funcFWHM, iSes, iRun] = deal(varargin{:}); diff --git a/src/subject_level/getFFXdir.m b/src/subject_level/getFFXdir.m index e2d4d4cc..677ec0cd 100644 --- a/src/subject_level/getFFXdir.m +++ b/src/subject_level/getFFXdir.m @@ -1,15 +1,13 @@ -% (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: @@ -17,15 +15,24 @@ % % :returns: - :ffxDir: (string) % + % (C) Copyright 2019 CPP_SPM developers + + glmDirName = createGlmDirName(opt, funcFWFM); - ffxDir = fullfile(opt.derivativesDir, ... - ['sub-', subID], ... + model = spm_jsonread(opt.model.file); + if ~isempty(model.Name) && ~strcmpi(model.Name, opt.taskName) + glmDirName = [glmDirName, '_desc-', convertToValidCamelCase(model.Name)]; + end + + ffxDir = fullfile(opt.dir.stats, ... + ['sub-', subLabel], ... 'stats', ... - ['ffx_task-', opt.taskName], ... - ['ffx_space-' opt.space '_FWHM-', num2str(funcFWFM)]); + glmDirName); - if ~exist(ffxDir, 'dir') - mkdir(ffxDir); + if opt.glm.roibased.do + ffxDir = [ffxDir '_roi']; end + spm_mkdir(ffxDir); + end diff --git a/src/subject_level/specifyContrasts.m b/src/subject_level/specifyContrasts.m index 811c6e27..7b2e5854 100644 --- a/src/subject_level/specifyContrasts.m +++ b/src/subject_level/specifyContrasts.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function contrasts = specifyContrasts(ffxDir, taskName, opt) % % Specifies the first level contrasts @@ -30,6 +28,8 @@ % Sn(1) R4 % Sn(1) R5 % Sn(1) R6 + % + % (C) Copyright 2019 CPP_SPM developers load(fullfile(ffxDir, 'SPM.mat')); diff --git a/src/templates/bidsTemplateWorkflow.m b/src/templates/bidsTemplateWorkflow.m new file mode 100644 index 00000000..2294c51b --- /dev/null +++ b/src/templates/bidsTemplateWorkflow.m @@ -0,0 +1,22 @@ +function bidsTemplateWorkflow(opt) + % + % + % (C) Copyright 2021 CPP_SPM developers + + [BIDS, opt] = setUpWorkflow(opt, 'workflow name'); + + for iSub = 1:numel(opt.subjects) + + subLabel = opt.subjects{iSub}; + + printProcessingSubject(iSub, subLabel); + + matlabbatch = []; + + % matlabbatch = setBatchSTC(matlabbatch, BIDS, opt, subLabel); + + saveAndRunWorkflow(matlabbatch, 'workflow name', opt, subLabel); + + end + +end diff --git a/src/templates/setBatchTemplate.m b/src/templates/setBatchTemplate.m index 7a7536b6..f17b0d03 100644 --- a/src/templates/setBatchTemplate.m +++ b/src/templates/setBatchTemplate.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function matlabbatch = setBatchTemplate(matlabbatch, BIDS, opt, subID, info, varargin) % % template to creae new setBatch functions @@ -12,6 +10,8 @@ % :type matlabbatch: % % :returns: - :matlabbatch: (structure) The matlabbatch ready to run the spm job + % + % (C) Copyright 2020 CPP_SPM developers printBatchName('name for this batch'); diff --git a/src/templates/templateFunction.m b/src/templates/templateFunction.m index 6897f2f2..c80726d3 100644 --- a/src/templates/templateFunction.m +++ b/src/templates/templateFunction.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function [argout1, argout2] = templateFunction(argin1, argin2, argin3) % % Short description of what the function does goes here. @@ -23,6 +21,8 @@ % % - item 1 % - item 2 + % + % (C) Copyright 2020 CPP_SPM developers % The code goes below diff --git a/src/templates/templateFunctionExample.m b/src/templates/templateFunctionExample.m index 941690b5..d0bd6970 100644 --- a/src/templates/templateFunctionExample.m +++ b/src/templates/templateFunctionExample.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function templateFunctionExample() % This function illustrates a documentation test defined for MOdox. % Other than that it does absolutely nothinghort description of what @@ -36,6 +34,8 @@ function templateFunctionExample() % % % % tests end here because test indentation has ended + % + % (C) Copyright 2020 CPP_SPM developers % The code goes below diff --git a/src/templates/templateGetOption.m b/src/templates/templateGetOption.m index 4946a976..797a3540 100644 --- a/src/templates/templateGetOption.m +++ b/src/templates/templateGetOption.m @@ -1,8 +1,9 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function opt = templateGetOption() + % % returns a structure that contains the options chosen by the user to return % the different workflows + % + % (C) Copyright 2020 CPP_SPM developers if nargin < 1 opt = []; diff --git a/src/unzipImgAndReturnsFullpathName.m b/src/unzipImgAndReturnsFullpathName.m index 189b7b01..0f4681c8 100644 --- a/src/unzipImgAndReturnsFullpathName.m +++ b/src/unzipImgAndReturnsFullpathName.m @@ -1,19 +1,22 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - 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) % + % (C) Copyright 2020 CPP_SPM developers [directory, filename, ext] = spm_fileparts(fullpathImgName); diff --git a/src/utils/checkDependencies.m b/src/utils/checkDependencies.m index ac9c46d4..dfb09d5b 100644 --- a/src/utils/checkDependencies.m +++ b/src/utils/checkDependencies.m @@ -1,33 +1,21 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function checkDependencies() % - % Checks that that the right dependencies are installed: - % - SPM - % - Nifti tools - % Also loads the spm defaults. + % Checks that that the right dependencies are installeda and + % loads the spm defaults. % % USAGE:: % % checkDependencies() % - % .. TODO: - % - % - need to check other dependencies (bids-matlab, spmup) % + % (C) Copyright 2019 CPP_SPM developers - 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 +28,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/cleanCrash.m b/src/utils/cleanCrash.m index c8cce61d..ebc44640 100644 --- a/src/utils/cleanCrash.m +++ b/src/utils/cleanCrash.m @@ -1,15 +1,14 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function cleanCrash() % % Removes any files left over from a previous unfinished run of the pipeline, - % like any *.png imgages + % like any ``*.png`` imgages % % USAGE:: % % cleanCrash() % % + % (C) Copyright 2020 CPP_SPM developers files = {'spm.*.png'}; 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/utils/createDataDictionary.m b/src/utils/createDataDictionary.m index 973b1c0d..f7073d11 100644 --- a/src/utils/createDataDictionary.m +++ b/src/utils/createDataDictionary.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function createDataDictionary(subFuncDataDir, fileName, nbColums) % % Short description of what the function does goes here. @@ -15,6 +13,7 @@ function createDataDictionary(subFuncDataDir, fileName, nbColums) % :param nbColums: Number of extra columns to add as censoring regressors. % :type nbColums: integer % + % (C) Copyright 2020 CPP_SPM developers namecColumns = { ... 'trans_x', ... diff --git a/src/utils/createDerivativeDir.m b/src/utils/createDerivativeDir.m index dd947ac0..2f4b7fef 100644 --- a/src/utils/createDerivativeDir.m +++ b/src/utils/createDerivativeDir.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function createDerivativeDir(opt) % % Creates the derivative folder if it does not exist. @@ -11,6 +9,7 @@ function createDerivativeDir(opt) % :param opt: Options chosen for the analysis. See ``checkOptions()``. % :type opt: structure % + % (C) Copyright 2019 CPP_SPM developers if ~exist(opt.derivativesDir, 'dir') mkdir(opt.derivativesDir); diff --git a/src/utils/createGlmDirName.m b/src/utils/createGlmDirName.m new file mode 100644 index 00000000..43e5dcd9 --- /dev/null +++ b/src/utils/createGlmDirName.m @@ -0,0 +1,9 @@ +function glmDirName = createGlmDirName(opt, FWHM) + % + % (C) Copyright 2021 CPP_SPM developers + + glmDirName = ['task-', opt.taskName, ... + '_space-' opt.space, ... + '_FWHM-', num2str(FWHM)]; + +end diff --git a/src/utils/getEnvInfo.m b/src/utils/getEnvInfo.m index 6d54cf54..0f8734ba 100644 --- a/src/utils/getEnvInfo.m +++ b/src/utils/getEnvInfo.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function [OS, GeneratedBy] = getEnvInfo() % % Gets information about the environement and operating system to help generate @@ -12,6 +10,7 @@ % :returns: :OS: (structure) (dimension) % :GeneratedBy: (structure) (dimension) % + % (C) Copyright 2020 CPP_SPM developers GeneratedBy(1).name = 'cpp_spm'; GeneratedBy(1).Version = getVersion(); diff --git a/src/utils/getSubjectList.m b/src/utils/getSubjectList.m new file mode 100644 index 00000000..9bb2a3ae --- /dev/null +++ b/src/utils/getSubjectList.m @@ -0,0 +1,88 @@ +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'}; + % + % (C) Copyright 2021 CPP_SPM developers + + 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/src/utils/getVersion.m b/src/utils/getVersion.m index a0ef2b7a..f3471a6a 100644 --- a/src/utils/getVersion.m +++ b/src/utils/getVersion.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function versionNumber = getVersion() % % Reads the version number of the pipeline from the txt file in the root of the @@ -11,6 +9,7 @@ % % :returns: :versionNumber: (string) Use semantic versioning format (like v0.1.0) % + % (C) Copyright 2020 CPP_SPM developers try versionNumber = fileread(fullfile(fileparts(mfilename('fullpath')), ... diff --git a/src/utils/isOctave.m b/src/utils/isOctave.m index 5fe03b9f..a0c14fa7 100644 --- a/src/utils/isOctave.m +++ b/src/utils/isOctave.m @@ -1,6 +1,3 @@ -% (C) Copyright 2020 Agah Karakuzu -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function retval = isOctave() % % Returns true if the environment is Octave. @@ -11,6 +8,8 @@ % % :returns: :retval: (boolean) % + % (C) Copyright 2020 Agah Karakuzu + % (C) Copyright 2020 CPP_SPM developers persistent cacheval % speeds up repeated calls diff --git a/src/utils/loadAndCheckOptions.m b/src/utils/loadAndCheckOptions.m index 1c10e0e0..5ee90829 100644 --- a/src/utils/loadAndCheckOptions.m +++ b/src/utils/loadAndCheckOptions.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function opt = loadAndCheckOptions(optionJsonFile) % % Loads the json file provided describing the options of an analysis. It then checks @@ -23,9 +21,13 @@ % .. TODO % % - add test for when the input is a structure. + % + % (C) Copyright 2020 CPP_SPM developers 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/manageWorkersPool.m b/src/utils/manageWorkersPool.m index b5a8c08b..76a1f3e6 100644 --- a/src/utils/manageWorkersPool.m +++ b/src/utils/manageWorkersPool.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function manageWorkersPool(action, opt) % % Check matlab version and opens pool of workers for parallel work. @@ -19,6 +17,7 @@ function manageWorkersPool(action, opt) % opt.parallelize.nbWorkers = 3; % opt.parallelize.killOnExit = true; % + % (C) Copyright 2020 CPP_SPM developers if ~opt.parallelize.do opt.parallelize.nbWorkers = 1; diff --git a/src/utils/printBatchName.m b/src/utils/printBatchName.m index 32471cfe..8d103b6d 100644 --- a/src/utils/printBatchName.m +++ b/src/utils/printBatchName.m @@ -1,6 +1,6 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function printBatchName(batchName) + % + % (C) Copyright 2019 CPP_SPM developers fprintf(1, '\n BUILDING JOB: %s\n', lower(batchName)); diff --git a/src/utils/printCredits.m b/src/utils/printCredits.m index 906fc1c5..d1122a28 100644 --- a/src/utils/printCredits.m +++ b/src/utils/printCredits.m @@ -1,6 +1,8 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function printCredits() + % + % TODO: use the .zenodo.json to load contributors + % + % (C) Copyright 2019 CPP_SPM developers versionNumber = getVersion(); @@ -10,11 +12,13 @@ function printCredits() 'Olivier Collignon', ... 'Ane Gurtubay', ... 'Marco Barilari', ... + 'Michele MacLean', ... + 'Federica Falagiarda ', ... 'Ceren Battal'}; 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('___________________________________________________________________________'); @@ -25,7 +29,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'); diff --git a/src/utils/printProcessingRun.m b/src/utils/printProcessingRun.m index 29769001..7ac315cb 100644 --- a/src/utils/printProcessingRun.m +++ b/src/utils/printProcessingRun.m @@ -1,6 +1,6 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function printProcessingRun(groupName, iSub, subID, iSes, iRun) + % + % (C) Copyright 2019 CPP_SPM developers fprintf(1, ... [ ... diff --git a/src/utils/printProcessingSubject.m b/src/utils/printProcessingSubject.m index cb5a27a5..d7131e87 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) + % + % (C) Copyright 2019 CPP_SPM developers 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/utils/printWorklowName.m b/src/utils/printWorklowName.m index 9b4155fd..395c08b8 100644 --- a/src/utils/printWorklowName.m +++ b/src/utils/printWorklowName.m @@ -1,6 +1,6 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function printWorklowName(workflowName) + % + % (C) Copyright 2019 CPP_SPM developers fprintf(1, '\n\n\nWORKFLOW: %s\n\n', upper(workflowName)); diff --git a/src/utils/removeSpmPrefix.m b/src/utils/removeSpmPrefix.m new file mode 100644 index 00000000..cb5a2ba2 --- /dev/null +++ b/src/utils/removeSpmPrefix.m @@ -0,0 +1,10 @@ +function image = removeSpmPrefix(image, prefix) + % + % (C) Copyright 2019 CPP_SPM developers + + basename = spm_file(image, 'basename'); + tmp = spm_file(image, 'basename', basename(length(prefix) + 1:end)); + movefile(image, tmp); + image = tmp; + +end diff --git a/src/utils/rmTrialTypeStr.m b/src/utils/rmTrialTypeStr.m index 82116dd6..cc5063c5 100644 --- a/src/utils/rmTrialTypeStr.m +++ b/src/utils/rmTrialTypeStr.m @@ -1,5 +1,6 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers function conName = rmTrialTypeStr(conName) + % + % (C) Copyright 2019 CPP_SPM developers conName = strrep(conName, 'trial_type.', ''); diff --git a/src/utils/saveMatlabBatch.m b/src/utils/saveMatlabBatch.m index 7d6c7ff0..82212ff4 100644 --- a/src/utils/saveMatlabBatch.m +++ b/src/utils/saveMatlabBatch.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function saveMatlabBatch(matlabbatch, batchType, opt, subID) % % Also save some basic environnment info. @@ -18,6 +16,7 @@ function saveMatlabBatch(matlabbatch, batchType, opt, subID) % :type subID: string % % + % (C) Copyright 2019 CPP_SPM developers if nargin < 4 || isempty(subID) subID = 'group'; diff --git a/src/utils/saveOptions.m b/src/utils/saveOptions.m index 8f221439..df3964c0 100644 --- a/src/utils/saveOptions.m +++ b/src/utils/saveOptions.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function saveOptions(opt) % % Short description of what the function does goes here. @@ -11,11 +9,20 @@ function saveOptions(opt) % :param opt: Options chosen for the analysis. See ``checkOptions()``. % :type opt: structure % + % (C) Copyright 2020 CPP_SPM developers + + optionDir = fullfile(pwd, 'cfg'); + [~, ~, ~] = mkdir(optionDir); + + taskString = ''; + if isfield(opt, 'taskName') + taskString = ['_task-', opt.taskName]; + end - filename = fullfile(pwd, ['options', ... - '_task-', opt.taskName, ... - '_date-' datestr(now, 'yyyymmddHHMM'), ... - '.json']); + filename = fullfile(optionDir, ['options', ... + taskString, ... + '_date-' datestr(now, 'yyyymmddHHMM'), ... + '.json']); jsonFormat.indent = ' '; spm_jsonwrite(filename, opt, jsonFormat); diff --git a/src/utils/setDefaultFields.m b/src/utils/setDefaultFields.m deleted file mode 100644 index 3f514ddb..00000000 --- a/src/utils/setDefaultFields.m +++ /dev/null @@ -1,64 +0,0 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - -function structure = setDefaultFields(structure, fieldsToSet) - % - % Short description of what the function does goes here. - % - % 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 - % - % :returns: - :argout1: (type) (dimension) - % - :argout2: (type) (dimension) - % - % structure = setDefaultFields(structure, fieldsToSet) - % - % recursively loop through the fields of a structure and sets a value if they don't exist - % - - if isempty(fieldsToSet) - return - end - - names = fieldnames(fieldsToSet); - - for j = 1:numel(structure) - - for i = 1:numel(names) - - thisField = fieldsToSet.(names{i}); - - if isfield(structure(j), names{i}) && isstruct(structure(j).(names{i})) - - structure(j).(names{i}) = ... - setDefaultFields(structure(j).(names{i}), fieldsToSet.(names{i})); - - else - - structure = setFieldToIfNotPresent( ... - structure, ... - names{i}, ... - thisField); - end - - end - - structure = orderfields(structure); - - end - -end - -function structure = setFieldToIfNotPresent(structure, fieldName, value) - if ~isfield(structure, fieldName) - structure.(fieldName) = value; - end -end diff --git a/src/utils/setFields.m b/src/utils/setFields.m new file mode 100644 index 00000000..a82a0d87 --- /dev/null +++ b/src/utils/setFields.m @@ -0,0 +1,74 @@ +function structure = setFields(structure, fieldsToSet, overwrite) + % + % 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, overwrite = false) + % + % :param structure: + % :type structure: + % :param fieldsToSet: + % :type fieldsToSet: string + % :param overwrite: + % :type overwrite: boolean + % + % :returns: - :structure: (structure) + % + % + % (C) Copyright 2020 CPP_SPM developers + + if isempty(fieldsToSet) + return + end + + if nargin < 3 || isempty(overwrite) + overwrite = false; + end + + names = fieldnames(fieldsToSet); + + for j = 1:numel(structure) + + for i = 1:numel(names) + + thisField = fieldsToSet.(names{i}); + + if isfield(structure(j), names{i}) && isstruct(structure(j).(names{i})) + + structure(j).(names{i}) = ... + setFields(structure(j).(names{i}), fieldsToSet.(names{i}), overwrite); + + else + + if ~overwrite + structure = setFieldToIfNotPresent( ... + structure, ... + names{i}, ... + thisField); + else + structure.(names{i}) = thisField; + + end + + end + + end + + structure = orderfields(structure); + + end + +end + +function structure = setFieldToIfNotPresent(structure, fieldName, value) + if ~isfield(structure, fieldName) + for i = 1:numel(structure) + structure(i).(fieldName) = value; + end + end +end diff --git a/src/utils/setGraphicWindow.m b/src/utils/setGraphicWindow.m index 2d75a2c6..943cc27d 100644 --- a/src/utils/setGraphicWindow.m +++ b/src/utils/setGraphicWindow.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function [interactiveWindow, graphWindow, cmdLine] = setGraphicWindow() % % Short description of what the function does goes here. @@ -19,6 +17,7 @@ % :returns: - :argout1: (type) (dimension) % - :argout2: (type) (dimension) % + % (C) Copyright 2019 CPP_SPM developers interactiveWindow = []; graphWindow = []; diff --git a/src/utils/validationInputFile.m b/src/utils/validationInputFile.m index 10cf7aec..23b7dbdd 100644 --- a/src/utils/validationInputFile.m +++ b/src/utils/validationInputFile.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function files = validationInputFile(dir, fileNamePattern, prefix) % % Looks for file name pattern in a given directory and returns all the files @@ -41,6 +39,7 @@ % % tissueProbaMaps = validationInputFile(anatDataDir, anatImage, 'c[12]'); % % + % (C) Copyright 2019 CPP_SPM developers % try to guess directory in case a fullpath filename was given if isempty(dir) diff --git a/src/utils/writeDatasetDescription.m b/src/utils/writeDatasetDescription.m index c7641e28..7699c682 100644 --- a/src/utils/writeDatasetDescription.m +++ b/src/utils/writeDatasetDescription.m @@ -1,6 +1,6 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function writeDatasetDescription(opt) + % + % (C) Copyright 2020 CPP_SPM developers oldDatasetDescription = spm_jsonread(fullfile(opt.derivativesDir, 'dataset_description.json')); @@ -8,7 +8,7 @@ function writeDatasetDescription(opt) if isfield(oldDatasetDescription, 'DatasetDOI') newDatasetDescription.SourceDatasets{1}.DOI = ... - oldDatasetDescription.DatasetDOI; + oldDatasetDescription.DatasetDOI; end spm_jsonwrite( ... diff --git a/src/workflows/bidsConcatBetaTmaps.m b/src/workflows/bidsConcatBetaTmaps.m index b2a6ff46..ed11cc87 100644 --- a/src/workflows/bidsConcatBetaTmaps.m +++ b/src/workflows/bidsConcatBetaTmaps.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function bidsConcatBetaTmaps(opt, funcFWHM, deleteIndBeta, deleteIndTmaps) % % Make 4D images of beta and t-maps for the MVPA. :: @@ -15,6 +13,7 @@ function bidsConcatBetaTmaps(opt, funcFWHM, deleteIndBeta, deleteIndTmaps) % :param deleteIndTmaps: decide to delete t-maps % :type funcFWHM: (boolean) % + % (C) Copyright 2019 CPP_SPM developers % delete individual Beta and tmaps if nargin < 3 @@ -22,55 +21,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..230eeb98 100644 --- a/src/workflows/bidsCopyRawFolder.m +++ b/src/workflows/bidsCopyRawFolder.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function bidsCopyRawFolder(opt, deleteZippedNii, modalitiesToCopy, unZip) % % Copies the folders from the ``raw`` folder to the @@ -25,6 +23,7 @@ function bidsCopyRawFolder(opt, deleteZippedNii, modalitiesToCopy, unZip) % :param unZip: % :type unZip: boolean % + % (C) Copyright 2019 CPP_SPM developers %% input variables default values @@ -66,54 +65,54 @@ function bidsCopyRawFolder(opt, deleteZippedNii, modalitiesToCopy, unZip) copyTsvJson(rawDir, derivativesDir); %% Loop through the groups, subjects, sessions - [group, opt, BIDS] = getData(opt, rawDir); - - for iGroup = 1:length(group) + if ismember(modalitiesToCopy, 'func') + [BIDS, opt] = getData(opt, rawDir); + else + [BIDS, opt] = getData(opt, rawDir, 'T1w'); + end - for iSub = 1:group(iGroup).numSub + for iSub = 1:numel(opt.subjects) - subID = group(iGroup).subNumber{iSub}; + subLabel = opt.subjects{iSub}; - subDir = returnSubjectDir(subID); + subDir = returnSubjectDir(subLabel); - 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, subID, opt, 'Sessions'); + [sessions, nbSessions] = getInfo(BIDS, subLabel, 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 +130,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 +163,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 +188,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); @@ -253,8 +252,8 @@ 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) + 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 n = load_untouch_nii(file); diff --git a/src/workflows/bidsCreateROI.m b/src/workflows/bidsCreateROI.m new file mode 100644 index 00000000..07127220 --- /dev/null +++ b/src/workflows/bidsCreateROI.m @@ -0,0 +1,90 @@ +function bidsCreateROI(opt) + % + % (C) Copyright 2021 CPP_SPM developers + + if nargin < 1 + opt = []; + end + + [BIDS, opt] = setUpWorkflow(opt, 'create ROI'); + + opt.dir.roi = [opt.derivativesDir '-roi']; + spm_mkdir(fullfile(opt.dir.roi, 'group')); + + opt.jobsDir = fullfile(opt.dir.roi, 'JOBS', opt.taskName); + + hemi = {'lh', 'rh'}; + + for iHemi = 1:numel(hemi) + + for iROI = 1:numel(opt.roi.name) + + extractRoiFromAtlas(fullfile(opt.dir.roi, 'group'), ... + opt.roi.atlas, ... + opt.roi.name{iROI}, ... + hemi{iHemi}); + + end + + end + + if any(strcmp(opt.roi.space, 'individual')) + + roiList = spm_select('FPlist', ... + fullfile(opt.dir.roi, 'group'), ... + '^space-.*_mask.nii$'); + + for iSub = 1:numel(opt.subjects) + + subLabel = opt.subjects{iSub}; + + printProcessingSubject(iSub, subLabel); + + %% inverse normalize + [anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt); + + deformation_field = spm_select('FPlist', anatDataDir, ['^iy_' anatImage '$']); + + matlabbatch = {}; + for iROI = 1:size(roiList, 1) + matlabbatch = setBatchNormalize(matlabbatch, ... + {deformation_field}, ... + nan(1, 3), ... + {roiList(iROI, :)}); + matlabbatch{end}.spm.spatial.normalise.write.woptions.bb = nan(2, 3); + end + + saveAndRunWorkflow(matlabbatch, 'inverseNormalize', opt, subLabel); + + %% move and rename file + spm_mkdir(opt.dir.roi, ['sub-' subLabel], 'roi'); + + roiList = spm_select('FPlist', ... + fullfile(opt.dir.roi, 'group'), ... + '^wspace.*_mask.nii.*$'); + + for iROI = 1:size(roiList, 1) + + roiImage = deblank(roiList(iROI, :)); + + p = bids.internal.parse_filename(spm_file(roiImage, 'filename')); + + nameStructure = struct( ... + 'sub', subLabel, ... + 'space', 'individual', ... + 'hemi', p.hemi, ... + 'desc', p.desc, ... + 'label', p.label, ... + 'type', 'mask', ... + 'ext', '.nii'); + newName = createFilename(nameStructure); + + movefile(roiImage, ... + fullfile(opt.dir.roi, ['sub-' subLabel], 'roi', newName)); + + end + + end + end + +end diff --git a/src/workflows/bidsCreateVDM.m b/src/workflows/bidsCreateVDM.m index b06b1e7e..c235682c 100644 --- a/src/workflows/bidsCreateVDM.m +++ b/src/workflows/bidsCreateVDM.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function bidsCreateVDM(opt) % % Creates the voxel displacement maps from the fieldmaps of a BIDS @@ -20,50 +18,41 @@ function bidsCreateVDM(opt) % Inspired from spmup ``spmup_BIDS_preprocess`` (@ commit 198c980d6d7520b1a99) % (URL missing) % + % (C) Copyright 2020 CPP_SPM developers - if nargin < 1 - opt = []; - end - - [BIDS, opt, group] = setUpWorkflow(opt, 'create voxel displacement map'); - - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) + [BIDS, opt] = setUpWorkflow(opt, 'create voxel displacement map'); - groupName = group(iGroup).name; + parfor iSub = 1:numel(opt.subjects) - parfor iSub = 1:group(iGroup).numSub + subLabel = opt.subjects{iSub}; - 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(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 843b0bf0..ceee4257 100644 --- a/src/workflows/bidsFFX.m +++ b/src/workflows/bidsFFX.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function bidsFFX(action, opt, funcFWHM) % % - builds the subject level fMRI model and estimates it. @@ -27,73 +25,85 @@ function bidsFFX(action, opt, funcFWHM) % For unsmoothed data ``funcFWHM = 0``, for smoothed data ``funcFWHM = ... mm``. % In this way we can make multiple ffx for different smoothing degrees. % - - if nargin < 3 - opt = []; + % (C) Copyright 2020 CPP_SPM developers + + if opt.glm.roibased.do + message = sprintf( ... + ['The option opt.glm.roibased.do is set to true.\n', ... + ' Change the option to false to use this workflow or\n', ... + ' use the bidsRoiBasedGLM workflow to run roi based GLM.']); + error(message); end - [BIDS, opt, group] = setUpWorkflow(opt, 'subject level GLM'); + [BIDS, opt] = setUpWorkflow(opt, 'subject level GLM'); + + opt.jobsDir = fullfile(opt.dir.stats, 'JOBS', opt.taskName); 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); - case 'specifyAndEstimate' + p = struct( ... + 'type', 'designmatrix', ... + 'ext', '.png', ... + 'sub', subLabel, ... + 'task', opt.taskName, ... + 'space', opt.space, ... + 'desc', 'before estimation'); - matlabbatch = setBatchSubjectLevelGLMSpec(matlabbatch, BIDS, opt, subID, funcFWHM); - matlabbatch = setBatchPrintFigure(matlabbatch, [ ... - 'sub-', subID, ... - '_task-', opt.taskName, ... - '_design_before_estimation']); - matlabbatch = setBatchEstimateModel(matlabbatch); - matlabbatch = setBatchPrintFigure(matlabbatch, [ ... - 'sub-', subID, ... - '_task-', opt.taskName, ... - '_design_after_estimation']); + matlabbatch = setBatchPrintFigure(matlabbatch, fullfile(getFFXdir(subLabel, ... + funcFWHM, ... + opt), ... + createFilename(p))); - batchName = ... - ['specify_estimate_ffx_task-', opt.taskName, ... - '_space-', opt.space, ... - '_FWHM-', num2str(funcFWHM)]; + matlabbatch = setBatchEstimateModel(matlabbatch, opt); - saveAndRunWorkflow(matlabbatch, batchName, opt, subID); + p.desc = 'after estimation'; + matlabbatch = setBatchPrintFigure(matlabbatch, fullfile(getFFXdir(subLabel, ... + funcFWHM, ... + opt), ... + createFilename(p))); + batchName = ... + ['specify_estimate_ffx_task-', opt.taskName, ... + '_space-', opt.space, ... + '_FWHM-', num2str(funcFWHM)]; + + saveAndRunWorkflow(matlabbatch, batchName, opt, subLabel); + + if opt.glm.QA.do plot_power_spectra_of_GLM_residuals( ... - getFFXdir(subID, funcFWHM, opt), ... + getFFXdir(subLabel, funcFWHM, opt), ... opt.metadata.RepetitionTime); - deleteResidualImages(getFFXdir(subID, funcFWHM, opt)); - - movefile(['sub-', subID, '_task-', opt.taskName, '_design_*'], ... - getFFXdir(subID, funcFWHM, opt)); + deleteResidualImages(getFFXdir(subLabel, funcFWHM, opt)); - case 'contrasts' + end - matlabbatch = setBatchSubjectLevelContrasts(matlabbatch, opt, subID, funcFWHM); + case 'contrasts' - batchName = ... - ['contrasts_ffx_task-', opt.taskName, ... - '_space-', opt.space, ... - '_FWHM-', num2str(funcFWHM)]; + matlabbatch = setBatchSubjectLevelContrasts(matlabbatch, opt, subLabel, funcFWHM); - saveAndRunWorkflow(matlabbatch, batchName, opt, subID); + batchName = ... + ['contrasts_ffx_task-', opt.taskName, ... + '_space-', opt.space, ... + '_FWHM-', num2str(funcFWHM)]; - end + saveAndRunWorkflow(matlabbatch, batchName, opt, subLabel); 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/src/workflows/bidsLesionSegmentation.m b/src/workflows/bidsLesionSegmentation.m new file mode 100755 index 00000000..af0fb8da --- /dev/null +++ b/src/workflows/bidsLesionSegmentation.m @@ -0,0 +1,34 @@ +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. + % + % (C) Copyright 2021 CPP_SPM developers + + [BIDS, opt] = setUpWorkflow(opt, 'lesion segmentation'); + + for iSub = 1:numel(opt.subjects) + + subLabel = opt.subjects{iSub}; + + printProcessingSubject(iSub, subLabel); + + matlabbatch = []; + matlabbatch = setBatchLesionSegmentation(matlabbatch, BIDS, opt, subLabel); + + saveAndRunWorkflow(matlabbatch, 'LesionSegmentation', opt, subLabel); + + copyFigures(BIDS, opt, subLabel); + + end + +end diff --git a/src/workflows/bidsRFX.m b/src/workflows/bidsRFX.m index c4990bec..fcb7e5cc 100644 --- a/src/workflows/bidsRFX.m +++ b/src/workflows/bidsRFX.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function bidsRFX(action, opt, funcFWHM, conFWHM) % % - smooths all contrast images created at the subject level @@ -30,10 +28,7 @@ function bidsRFX(action, opt, funcFWHM, conFWHM) % - case ``RFX``: Mean Struct, MeanMask, Factorial design specification and % estimation, Contrast estimation % - - if nargin < 2 - opt = []; - end + % (C) Copyright 2020 CPP_SPM developers if nargin < 4 || isempty(funcFWHM) funcFWHM = 0; @@ -43,14 +38,16 @@ function bidsRFX(action, opt, funcFWHM, conFWHM) conFWHM = 0; end - [~, opt, group] = setUpWorkflow(opt, 'group level GLM'); + [~, opt] = setUpWorkflow(opt, 'group level GLM'); + + opt.jobsDir = fullfile(opt.dir.stats, 'JOBS', opt.taskName); 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], ... @@ -72,7 +69,7 @@ function bidsRFX(action, opt, funcFWHM, conFWHM) matlabbatch = setBatchMeanAnatAndMask(matlabbatch, ... opt, ... funcFWHM, ... - fullfile(opt.derivativesDir, 'group')); + fullfile(opt.dir.stats, 'group')); saveAndRunWorkflow(matlabbatch, 'create_mean_struc_mask', opt); % TODO @@ -82,7 +79,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); diff --git a/src/workflows/bidsRealignReslice.m b/src/workflows/bidsRealignReslice.m index 66f46cbf..0b90bb8d 100644 --- a/src/workflows/bidsRealignReslice.m +++ b/src/workflows/bidsRealignReslice.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function bidsRealignReslice(opt) % % Realigns and reslices the functional data of a given task. @@ -14,40 +12,28 @@ function bidsRealignReslice(opt) % % Assumes that ``bidsSTC()`` has already been run. % + % (C) Copyright 2020 CPP_SPM developers - if nargin < 1 - opt = []; - end - - [BIDS, opt, group] = setUpWorkflow(opt, 'realign and reslice'); - - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) - - groupName = group(iGroup).name; + [BIDS, opt] = setUpWorkflow(opt, 'realign and reslice'); - parfor iSub = 1:group(iGroup).numSub + parfor iSub = 1:numel(opt.subjects) - % 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 + subLabel = opt.subjects{iSub}; - printProcessingSubject(groupName, iSub, subID); + printProcessingSubject(iSub, subLabel); - matlabbatch = []; - [matlabbatch, ~] = setBatchRealign( ... - matlabbatch, ... - BIDS, ... - subID, ... - opt, ... - 'realignReslice'); + matlabbatch = []; + [matlabbatch, ~] = setBatchRealign( ... + matlabbatch, ... + BIDS, ... + subLabel, ... + opt, ... + 'realignReslice'); - saveAndRunWorkflow(matlabbatch, 'realign_reslice', opt, subID); + saveAndRunWorkflow(matlabbatch, 'realign_reslice', opt, subLabel); - copyFigures(BIDS, opt, subID); + copyFigures(BIDS, opt, subLabel); - end end end diff --git a/src/workflows/bidsRealignUnwarp.m b/src/workflows/bidsRealignUnwarp.m index e8db8d0b..c56387a6 100644 --- a/src/workflows/bidsRealignUnwarp.m +++ b/src/workflows/bidsRealignUnwarp.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function bidsRealignUnwarp(opt) % % Realigns and unwarps the functional data of a given task. @@ -17,40 +15,28 @@ function bidsRealignUnwarp(opt) % If the ``bidsCreateVDM()`` workflow has been run before the voxel displacement % maps will be used unless ``opt.useFieldmaps`` is set to ``false``. % + % (C) Copyright 2020 CPP_SPM developers - if nargin < 1 - opt = []; - end - - [BIDS, opt, group] = setUpWorkflow(opt, 'realign and unwarp'); - - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) - - groupName = group(iGroup).name; + [BIDS, opt] = setUpWorkflow(opt, 'realign and unwarp'); - parfor iSub = 1:group(iGroup).numSub + parfor iSub = 1:numel(opt.subjects) - % 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 + subLabel = opt.subjects{iSub}; - printProcessingSubject(groupName, iSub, subID); + printProcessingSubject(iSub, subLabel); - matlabbatch = []; - [matlabbatch, ~] = setBatchRealign( ... - matlabbatch, ... - BIDS, ... - subID, ... - opt, ... - 'realignUnwarp'); + matlabbatch = []; + [matlabbatch, ~] = setBatchRealign( ... + matlabbatch, ... + BIDS, ... + subLabel, ... + opt, ... + 'realignUnwarp'); - saveAndRunWorkflow(matlabbatch, 'realign_unwarp', opt, subID); + saveAndRunWorkflow(matlabbatch, 'realign_unwarp', opt, subLabel); - copyFigures(BIDS, opt, subID); + copyFigures(BIDS, opt, subLabel); - end end end diff --git a/src/workflows/bidsResliceTpmToFunc.m b/src/workflows/bidsResliceTpmToFunc.m index 0421e856..3394c54f 100644 --- a/src/workflows/bidsResliceTpmToFunc.m +++ b/src/workflows/bidsResliceTpmToFunc.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function bidsResliceTpmToFunc(opt) % % Reslices the tissue probability map (TPMs) from the segmentation to the mean @@ -20,57 +18,46 @@ function bidsResliceTpmToFunc(opt) % as the computation of the tSNR by ``spmup`` requires the TPMs to have the same dimension % as the functional. % + % (C) Copyright 2020 CPP_SPM developers - if nargin < 1 - opt = []; - end - - [BIDS, opt, group] = setUpWorkflow(opt, ... - 'reslicing tissue probability maps to functional dimension'); - - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) - - groupName = group(iGroup).name; - - for iSub = 1:group(iGroup).numSub + [BIDS, opt] = setUpWorkflow(opt, 'reslicing tissue probability maps to functional dimension'); - subID = group(iGroup).subNumber{iSub}; + for iSub = 1:numel(opt.subjects) - printProcessingSubject(groupName, iSub, subID); + subLabel = opt.subjects{iSub}; - [meanImage, meanFuncDir] = getMeanFuncFilename(BIDS, subID, opt); + printProcessingSubject(iSub, subLabel); - % get grey and white matter and CSF tissue probability maps - [anatImage, anatDataDir] = getAnatFilename(BIDS, subID, opt); - TPMs = validationInputFile(anatDataDir, anatImage, 'c[123]'); + [meanImage, meanFuncDir] = getMeanFuncFilename(BIDS, subLabel, opt); - matlabbatch = []; - matlabbatch = setBatchReslice(matlabbatch, ... - fullfile(meanFuncDir, meanImage), ... - cellstr(TPMs)); + % get grey and white matter and CSF tissue probability maps + [anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt); + TPMs = validationInputFile(anatDataDir, anatImage, 'c[123]'); - saveAndRunWorkflow(matlabbatch, 'reslice_tpm', opt, subID); + 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, subLabel); - 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); - saveAndRunWorkflow(matlabbatch, 'create_functional_brain_mask', opt, subID); + matlabbatch = []; + matlabbatch = setBatchImageCalculation(matlabbatch, input, output, meanFuncDir, expression); - end + saveAndRunWorkflow(matlabbatch, 'create_functional_brain_mask', opt, subLabel); end diff --git a/src/workflows/bidsResults.m b/src/workflows/bidsResults.m index 05c00a88..ae744de4 100644 --- a/src/workflows/bidsResults.m +++ b/src/workflows/bidsResults.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function bidsResults(opt, funcFWHM, conFWHM) % % Computes the results for a series of contrast that can be @@ -20,14 +18,13 @@ function bidsResults(opt, funcFWHM, conFWHM) % images (Gaussian kernel size). % :type conFWHM: scalar % + % (C) Copyright 2020 CPP_SPM developers - if nargin < 1 - opt = []; - end - - [~, opt, group] = setUpWorkflow(opt, 'computing GLM results'); + [BIDS, opt] = setUpWorkflow(opt, 'computing GLM results'); - matlabbatch = []; + if isempty(opt.model.file) + opt = createDefaultModel(BIDS, opt); + end % TOD0 % if it does not exist create the default "result" field from the BIDS model file @@ -43,44 +40,50 @@ function bidsResults(opt, funcFWHM, conFWHM) case 'run' warning('run level not implemented yet'); + matlabbatch = []; % saveMatlabBatch(matlabbatch, 'computeFfxResults', opt, subID); 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 + matlabbatch = []; - for iCon = 1:length(opt.result.Steps(iStep).Contrasts) + subLabel = opt.subjects{iSub}; - % Get the Subject ID - subID = group(iGroup).subNumber{iSub}; + results.dir = getFFXdir(subLabel, funcFWHM, opt); - matlabbatch = ... - setBatchSubjectLevelResults( ... - matlabbatch, ... - opt, ... - subID, ... - funcFWHM, ... - iStep, ... - iCon); + for iCon = 1:length(opt.result.Steps(iStep).Contrasts) - end + matlabbatch = ... + setBatchSubjectLevelResults( ... + matlabbatch, ... + opt, ... + subLabel, ... + funcFWHM, ... + iStep, ... + iCon); end - batchName = sprintf('compute_sub-%s_results', subID); + batchName = sprintf('compute_sub-%s_results', subLabel); + + saveAndRunWorkflow(matlabbatch, batchName, opt, subLabel); + + renameOutputResults(results); - saveAndRunWorkflow(matlabbatch, batchName, opt, subID); + renamePng(results); end case 'dataset' + matlabbatch = []; + 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; @@ -110,3 +113,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 diff --git a/src/workflows/bidsRoiBasedGLM.m b/src/workflows/bidsRoiBasedGLM.m new file mode 100644 index 00000000..948a5a96 --- /dev/null +++ b/src/workflows/bidsRoiBasedGLM.m @@ -0,0 +1,126 @@ +function bidsRoiBasedGLM(opt) + % + % Will run a GLM within a ROI using MarsBar. + % + % Will compute the percent signal change and the time course of the events + % or blocks of contrast specified in the BIDS model. + % + % (C) Copyright 2021 CPP_SPM developers + + if ~opt.glm.roibased.do + message = sprintf( ... + ['The option opt.glm.roibased.do is set to false.\n', ... + ' Change the option to true to use this workflow or\n', ... + ' use the bidsFFX workflow to run whole brain GLM.']); + error(message); + end + + funcFWHM = 0; + + [BIDS, opt] = setUpWorkflow(opt, 'roi based glm'); + + opt.space = 'individual'; + opt.jobsDir = fullfile(opt.dir.stats, 'JOBS', opt.taskName); + + if isempty(opt.model.file) + opt = createDefaultModel(BIDS, opt); + end + + for iSub = 1:numel(opt.subjects) + + subLabel = opt.subjects{iSub}; + + printProcessingSubject(iSub, subLabel); + + matlabbatch = []; + + matlabbatch = setBatchSubjectLevelGLMSpec(matlabbatch, BIDS, opt, subLabel, funcFWHM); + + batchName = ['specify_roi_based_GLM_task-', opt.taskName]; + + saveAndRunWorkflow(matlabbatch, batchName, opt, subLabel); + + load(fullfile(getFFXdir(subLabel, funcFWHM, opt), 'SPM.mat')); + + nbRuns = numel(SPM.Sess); + + conditions = {}; + runs = []; + durations = []; + for iRun = 1:nbRuns + tmp = cat(2, SPM.Sess(iRun).U(:).name); + conditions = cat(2, conditions, tmp); + runs = [runs ones(size(tmp)) * iRun]; + for iCdt = 1:numel(tmp) + durations = [durations mean(SPM.Sess(iRun).U(iCdt).dur)]; + end + end + + names = unique(conditions); + events = []; + for iEvent = 1:numel(conditions) + events(end + 1) = find(strcmp(conditions(iEvent), names)); + end + + roiList = spm_select('FPList', ... + fullfile(opt.dir.roi, ['sub-' subLabel], 'roi'), ... + '^sub-.*_mask.nii$'); + + model = mardo(SPM); + + for iROI = 1:size(roiList, 1) + + roiImage = deblank(roiList(iROI, :)); + + % create ROI object for Marsbar + % and convert to matrix format to avoid delicacies of image format + roiObject = maroi_image(struct( ... + 'vol', spm_vol(roiImage), ... + 'binarize', true, ... + 'func', [])); + roiObject = maroi_matrix(roiObject); + + % Extract data and do MarsBaR estimation + data = get_marsy(roiObject, model, 'mean'); + estimation = estimate(model, data); + + % -------------------- IMPROVE ------------------------ % + + % currently this only computes this averages over all all events + % we will want to use the bids model to know which event to fit + % based on the contrasts. + + % Fitted time courses + [tc, dt] = event_fitted(estimation, [runs; events], durations); + + % Get percent signal change + psc = event_signal(estimation, [runs; events], durations, 'abs max'); + + % -------------------- IMPROVE ------------------------ % + + p = bids.internal.parse_filename(spm_file(roiImage, 'filename')); + fields = {'hemi', 'desc', 'label'}; + for iField = 1:numel(fields) + if ~isfield(p, fields{iField}) + p.(fields{iField}) = ''; + end + end + nameStructure = struct( ... + 'sub', subLabel, ... + 'task', opt.taskName, ... + 'space', 'individual', ... + 'hemi', p.hemi, ... + 'desc', p.desc, ... + 'label', p.label, ... + 'type', 'estimates', ... + 'ext', '.mat'); + newName = createFilename(nameStructure); + + save(fullfile(getFFXdir(subLabel, funcFWHM, opt), newName), ... + 'estimation', 'tc', 'dt', 'psc'); + + end + + end + +end diff --git a/src/workflows/bidsSTC.m b/src/workflows/bidsSTC.m index ade9c400..76c19eb5 100644 --- a/src/workflows/bidsSTC.m +++ b/src/workflows/bidsSTC.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function bidsSTC(opt) % % Performs the slie timing correction of the functional data. @@ -29,30 +27,21 @@ function bidsSTC(opt) % % See the documentation for more information about slice timing correction. % + % (C) Copyright 2019 CPP_SPM developers - if nargin < 1 - opt = []; - end - - [BIDS, opt, group] = setUpWorkflow(opt, 'slice timing correction'); - - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) - - groupName = group(iGroup).name; + [BIDS, opt] = setUpWorkflow(opt, 'slice timing correction'); - parfor iSub = 1:group(iGroup).numSub + parfor iSub = 1:numel(opt.subjects) - subID = group(iGroup).subNumber{iSub}; + subLabel = opt.subjects{iSub}; - printProcessingSubject(groupName, iSub, subID); + printProcessingSubject(iSub, subLabel); - matlabbatch = []; - matlabbatch = setBatchSTC(matlabbatch, BIDS, opt, subID); + matlabbatch = []; + matlabbatch = setBatchSTC(matlabbatch, BIDS, opt, subLabel); - saveAndRunWorkflow(matlabbatch, 'STC', opt, subID); + saveAndRunWorkflow(matlabbatch, 'STC', opt, subLabel); - end end end diff --git a/src/workflows/bidsSegmentSkullStrip.m b/src/workflows/bidsSegmentSkullStrip.m index 1e2da98a..978e2b0c 100644 --- a/src/workflows/bidsSegmentSkullStrip.m +++ b/src/workflows/bidsSegmentSkullStrip.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function bidsSegmentSkullStrip(opt) % % Segments and skullstrips the anatomical image. @@ -12,37 +10,29 @@ function bidsSegmentSkullStrip(opt) % ``checkOptions()`` and ``loadAndCheckOptions()``. % :type opt: structure % + % (C) Copyright 2020 CPP_SPM developers - if nargin < 1 - opt = []; - end - - [BIDS, opt, group] = setUpWorkflow(opt, 'segmentation and skulltripping'); - - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) + [BIDS, opt] = setUpWorkflow(opt, 'segmentation and skulltripping'); - groupName = group(iGroup).name; + opt.orderBatches.selectAnat = 1; + opt.orderBatches.segment = 2; - for iSub = 1:group(iGroup).numSub + parfor iSub = 1:numel(opt.subjects) - subID = group(iGroup).subNumber{iSub}; + subLabel = opt.subjects{iSub}; - printProcessingSubject(groupName, iSub, subID); + printProcessingSubject(iSub, subLabel); - matlabbatch = []; - matlabbatch = setBatchSelectAnat(matlabbatch, BIDS, opt, subID); - opt.orderBatches.selectAnat = 1; + matlabbatch = []; + matlabbatch = setBatchSelectAnat(matlabbatch, BIDS, opt, subLabel); - % dependency from file selector ('Anatomical') - matlabbatch = setBatchSegmentation(matlabbatch, opt); - opt.orderBatches.segment = 2; + % dependency from file selector ('Anatomical') + matlabbatch = setBatchSegmentation(matlabbatch, opt); - matlabbatch = setBatchSkullStripping(matlabbatch, BIDS, subID, opt); + matlabbatch = setBatchSkullStripping(matlabbatch, BIDS, opt, subLabel); - saveAndRunWorkflow(matlabbatch, 'segment_skullstrip', opt, subID); + saveAndRunWorkflow(matlabbatch, 'segment_skullstrip', opt, subLabel); - end end end diff --git a/src/workflows/bidsSmoothing.m b/src/workflows/bidsSmoothing.m index 2dd19d46..ceaa5710 100644 --- a/src/workflows/bidsSmoothing.m +++ b/src/workflows/bidsSmoothing.m @@ -1,5 +1,3 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers - function bidsSmoothing(funcFWHM, opt) % % This performs smoothing to the functional data using a full width @@ -7,7 +5,7 @@ function bidsSmoothing(funcFWHM, opt) % % USAGE:: % - % bidsSmoothing(funcFWHM, [opt]) + % bidsSmoothing(funcFWHM, [opt]) % % :param funcFWHM: How much smoothing was applied to the functional % data in the preprocessing (Gaussian kernel size). @@ -15,32 +13,22 @@ function bidsSmoothing(funcFWHM, opt) % :param opt: structure or json filename containing the options. See % ``checkOptions()`` and ``loadAndCheckOptions()``. % :type opt: structure - % + % (C) Copyright 2020 CPP_SPM developers - if nargin < 2 - opt = []; - end - - [BIDS, opt, group] = setUpWorkflow(opt, 'smoothing functional data'); - - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) - - groupName = group(iGroup).name; + [BIDS, opt] = setUpWorkflow(opt, 'smoothing functional data'); - parfor iSub = 1:group(iGroup).numSub + parfor iSub = 1:numel(opt.subjects) - subID = group(iGroup).subNumber{iSub}; + subLabel = opt.subjects{iSub}; - printProcessingSubject(groupName, iSub, subID); + printProcessingSubject(iSub, subLabel); - matlabbatch = []; - matlabbatch = setBatchSmoothingFunc(matlabbatch, BIDS, opt, subID, funcFWHM); + matlabbatch = []; + matlabbatch = setBatchSmoothingFunc(matlabbatch, BIDS, opt, subLabel, funcFWHM); - saveAndRunWorkflow(matlabbatch, ['smoothing_FWHM-' num2str(funcFWHM)], opt, subID); + saveAndRunWorkflow(matlabbatch, ['smoothing_FWHM-' num2str(funcFWHM)], opt, subLabel); - end end end diff --git a/src/workflows/bidsSpatialPrepro.m b/src/workflows/bidsSpatialPrepro.m index 91e73a34..b144192c 100644 --- a/src/workflows/bidsSpatialPrepro.m +++ b/src/workflows/bidsSpatialPrepro.m @@ -1,5 +1,3 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers - function bidsSpatialPrepro(opt) % % Performs spatial preprocessing of the functional and structural data. @@ -37,12 +35,9 @@ function bidsSpatialPrepro(opt) % % - average T1s across sessions if necessarry % + % (C) Copyright 2019 CPP_SPM developers - if nargin < 1 - 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 +47,51 @@ function bidsSpatialPrepro(opt) opt.orderBatches.skullStripping = 6; opt.orderBatches.skullStrippingMask = 7; - %% Loop through the groups, subjects, and sessions - for iGroup = 1:length(group) - - groupName = group(iGroup).name; + parfor iSub = 1:numel(opt.subjects) - parfor iSub = 1:group(iGroup).numSub + matlabbatch = []; - 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}; + subLabel = opt.subjects{iSub}; - printProcessingSubject(groupName, iSub, subID); + printProcessingSubject(iSub, subLabel); - matlabbatch = setBatchSelectAnat(matlabbatch, BIDS, opt, subID); + matlabbatch = setBatchSelectAnat(matlabbatch, BIDS, opt, subLabel); - % 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..e652ca6e 100644 --- a/src/workflows/saveAndRunWorkflow.m +++ b/src/workflows/saveAndRunWorkflow.m @@ -1,6 +1,4 @@ -% (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 % @@ -17,14 +15,16 @@ function saveAndRunWorkflow(matlabbatch, batchName, opt, subID) % :type opt: structure % :param subID: subject ID % :type subID: string + % + % (C) Copyright 2019 CPP_SPM developers 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..85fd5e85 100644 --- a/src/workflows/setUpWorkflow.m +++ b/src/workflows/setUpWorkflow.m @@ -1,6 +1,4 @@ -% (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, @@ -24,11 +22,12 @@ % - :opt: options checked % - :group: % + % (C) Copyright 2019 CPP_SPM developers opt = loadAndCheckOptions(opt); % load the subjects/Groups information and the task name - [group, opt, BIDS] = getData(opt); + [BIDS, opt] = getData(opt); cleanCrash(); diff --git a/tests/README.md b/tests/README.md index 28db08aa..9a2931f3 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,10 +1,57 @@ -# README +# Tests for CPP_SPM -```matlab -coverage = mocov( ... - '-expression', 'moxunit_runtests()', ... - '-verbose', ... - '-cover', fullfile(pwd, '..', 'subfun'), .... - '-cover_xml_file', 'coverage.xml', ... - '-cover_html_dir', 'coverage_html') +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). + +See +[HERE](https://github.com/cpp-lln-lab/.github/blob/main/CONTRIBUTING.md#how-to-run-the-tests) +for general information on how to run the tests. + +## 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 tests + +See +[HERE](https://github.com/cpp-lln-lab/.github/blob/main/CONTRIBUTING.md#adding-more-tests) +to add more tests + +### Style guidelines + +#### Filenames + +- unit tests names: test*unit*\* + +- other tests names: test\_\* diff --git a/tests/createDummyDataSet.sh b/tests/createDummyDataSet.sh index 89d39117..d38a692e 100755 --- a/tests/createDummyDataSet.sh +++ b/tests/createDummyDataSet.sh @@ -4,25 +4,28 @@ # defines where the BIDS data set will be created StartDir=`pwd` # relative to starting directory -StartDir=$StartDir/dummyData/derivatives/cpp_spm -mkdir $StartDir +PrerpoDir=$StartDir/dummyData/derivatives/cpp_spm +StatsDir=$StartDir/dummyData/derivatives/cpp_spm-stats + +mkdir $StatsDir SubList='ctrl01 ctrl02 blind01 blind02 01 02' # subject list SesList='01 02' # session list -for Subject in $SubList # loop through subjects +for Subject in $SubList do - mkdir $StartDir/sub-$Subject # create folder for subject + mkdir $PrerpoDir/sub-$Subject + mkdir $StatsDir/sub-$Subject - for Ses in $SesList # loop through sessions + for Ses in $SesList do # create folder for each session and functional and fmap - mkdir $StartDir/sub-$Subject/ses-$Ses + mkdir $PrerpoDir/sub-$Subject/ses-$Ses # FUNC - ThisDir=$StartDir/sub-$Subject/ses-$Ses/func + ThisDir=$PrerpoDir/sub-$Subject/ses-$Ses/func mkdir $ThisDir touch $ThisDir/sub-$Subject\_ses-$Ses\_task-vismotion_run-1_bold.nii @@ -32,6 +35,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 @@ -55,7 +61,7 @@ do echo "6\t2\tVisMotUp" >> $ThisDir/sub-$Subject\_ses-$Ses\_task-vismotion_run-2_events.tsv # FMAP - ThisDir=$StartDir/sub-$Subject/ses-$Ses/fmap + ThisDir=$PrerpoDir/sub-$Subject/ses-$Ses/fmap mkdir $ThisDir touch $ThisDir/sub-$Subject\_ses-$Ses\_run-1_phasediff.nii @@ -80,7 +86,7 @@ do done # ANAT - ThisDir=$StartDir/sub-$Subject/ses-01/anat + ThisDir=$PrerpoDir/sub-$Subject/ses-01/anat mkdir $ThisDir touch $ThisDir/sub-$Subject\_ses-01_T1w.nii @@ -91,12 +97,11 @@ do touch $ThisDir/c3sub-$Subject\_ses-01_T1w.nii # 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 + mkdir $StatsDir/sub-$Subject/stats + ThisDir=$StatsDir/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 dummyData/SPM.mat $ThisDir/SPM.mat touch $ThisDir/mask.nii diff --git a/tests/dummyData/derivatives/cpp_spm/sub-01/stats/ffx_task-vismotion/ffx_space-MNI_FWHM-6/SPM.mat b/tests/dummyData/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/SPM.mat 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/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/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/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-visMotionLoc_smdl.json b/tests/dummyData/models/model-visMotionLoc_smdl.json deleted file mode 100644 index aca51a49..00000000 --- a/tests/dummyData/models/model-visMotionLoc_smdl.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "Name": "Motion localizer", - "Description": "contrasts for the motion localizer dataset", - "Input": { - "task": "vismotion" - }, - "Steps": [ - { - "Level": "subject", - "AutoContrasts": ["trial_type.VisMot", "trial_type.VisStat" ], - "Contrasts": [ - { - "Name": "VisMot_gt_VisStat", - "ConditionList": [ - "trial_type.VisMot", "trial_type.VisStat" - ], - "weights": [1, -1], - "type": "t" - }, - { - "Name": "VisStat_gt_VisMot", - "ConditionList": [ - "trial_type.VisMot", "trial_type.VisStat" - ], - "weights": [-1, 1], - "type": "t" - } - ] - }, - { - "Level": "dataset", - "AutoContrasts": ["trial_type.VisMot", "trial_type.VisStat", "VisMot_gt_VisStat", "VisStat_gt_VisMot"] - } - ] -} 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-vismotion_smdl.json b/tests/dummyData/models/model-vismotion_smdl.json new file mode 100644 index 00000000..7c8dd346 --- /dev/null +++ b/tests/dummyData/models/model-vismotion_smdl.json @@ -0,0 +1,51 @@ +{ + "Name": "vismotion", + "Description": "contrasts for the motion dataset", + "Input": { + "task": "vismotion" + }, + "Steps": [ + { + "Level": "subject", + "AutoContrasts": [ + "trial_type.VisMot", + "trial_type.VisStat" + ], + "Contrasts": [ + { + "Name": "VisMot_gt_VisStat", + "ConditionList": [ + "trial_type.VisMot", + "trial_type.VisStat" + ], + "weights": [ + 1, + -1 + ], + "type": "t" + }, + { + "Name": "VisStat_gt_VisMot", + "ConditionList": [ + "trial_type.VisMot", + "trial_type.VisStat" + ], + "weights": [ + -1, + 1 + ], + "type": "t" + } + ] + }, + { + "Level": "dataset", + "AutoContrasts": [ + "trial_type.VisMot", + "trial_type.VisStat", + "VisMot_gt_VisStat", + "VisStat_gt_VisMot" + ] + } + ] +} \ No newline at end of file diff --git a/tests/miss_hit.cfg b/tests/miss_hit.cfg index 1aa3765a..684e5239 100644 --- a/tests/miss_hit.cfg +++ b/tests/miss_hit.cfg @@ -1,10 +1 @@ -# 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" - -# metrics limit for the code quality (https://florianschanda.github.io/miss_hit/metrics.html) -metric "cnest": limit 4 -metric "file_length": limit 500 -metric "cyc": limit 15 -metric "parameters": limit 5 \ No newline at end of file +regex_function_name: "(test(_unit){0,1}_[a-z]+|[a-z]+)(([A-Z]){1}[A-Za-z0-9]+)*" diff --git a/tests/test_bidsCopyRawFolder.m b/tests/test_bidsCopyRawFolder.m index ad8adadc..b9d6fb4d 100644 --- a/tests/test_bidsCopyRawFolder.m +++ b/tests/test_bidsCopyRawFolder.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_bidsCopyRawFolder %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -8,13 +10,7 @@ function test_bidsCopyRawFolderBasic() - opt.dataDir = fullfile( ... - fileparts(mfilename('fullpath')), ... - '..', 'demos', 'MoAE', 'output', 'MoAEpilot'); - - opt.taskName = 'auditory'; - - opt = checkOptions(opt); + opt = setOptions('MoAE'); bidsCopyRawFolder(opt, 1); @@ -54,8 +50,6 @@ function test_bidsCopyRawFolder2tasks() opt.taskName = 'vismotion'; - opt = checkOptions(opt); - unZip = false; deleteZippedNii = false; bidsCopyRawFolder(opt, deleteZippedNii, {'func'}, unZip); diff --git a/tests/test_checkOptions.m b/tests/test_checkOptions.m index eab892fe..383c71f0 100644 --- a/tests/test_checkOptions.m +++ b/tests/test_checkOptions.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_checkOptions %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -11,8 +13,7 @@ function test_checkOptionsBasic() opt.taskName = 'testTask'; opt = checkOptions(opt); - expectedOptions = defaultOptions(); - expectedOptions.taskName = 'testTask'; + expectedOptions = defaultOptions('testTask'); assertEqual(opt, expectedOptions); @@ -74,43 +75,13 @@ function test_checkOptionsErrorVoxDim() 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; +function test_checkOptionsSessionString() - 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; + opt.taskName = 'testTask'; + opt.anatReference.session = 1; - expectedOptions = orderfields(expectedOptions); + assertExceptionThrown( ... + @()checkOptions(opt), ... + 'checkOptions:sessionNotString'); 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_createAndReturnOnsetFile.m b/tests/test_createAndReturnOnsetFile.m index a3189511..6c10563f 100644 --- a/tests/test_createAndReturnOnsetFile.m +++ b/tests/test_createAndReturnOnsetFile.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_createAndReturnOnsetFile %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -8,33 +10,26 @@ 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.model.file = fullfile(fileparts(mfilename('fullpath')), ... - 'dummyData', 'models', ... - 'model-vislocalizer_smdl.json'); - - opt = checkOptions(opt); + opt = setOptions('vislocalizer', subLabel); - [~, 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', ... - 'ffx_task-vislocalizer', 'ffx_space-MNI_FWHM-6', ... - 'onsets_sub-01_ses-01_task-vislocalizer_events.mat'); + 'dummyData', 'derivatives', 'cpp_spm-stats', 'sub-01', 'stats', ... + 'task-vislocalizer_space-MNI_FWHM-6', ... + 'sub-01_ses-01_task-vislocalizer_space-MNI_onsets.mat'); assertEqual(exist(onsetFileName, 'file'), 2); assertEqual(exist(expectedFileName, 'file'), 2); diff --git a/tests/test_createDefaultModel.m b/tests/test_createDefaultModel.m index b10f963b..e508f1ba 100644 --- a/tests/test_createDefaultModel.m +++ b/tests/test_createDefaultModel.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_createDefaultModel %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -8,12 +10,9 @@ function test_createDefaultModelBasic() - opt.taskName = 'vislocalizer'; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - - opt = checkOptions(opt); + opt = setOptions('vislocalizer'); - [~, 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..94e9de88 100644 --- a/tests/test_getAnatFilename.m +++ b/tests/test_getAnatFilename.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_getAnatFilename %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -9,23 +11,18 @@ % 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() - subID = '01'; - - opt.taskName = 'vislocalizer'; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.groups = {''}; - opt.subjects = {subID}; + subLabel = '01'; - opt = checkOptions(opt); + opt = setOptions('vislocalizer', subLabel); - [~, 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'; @@ -36,4 +33,49 @@ function test_getAnatFilenameBasic() assertEqual(anatDataDir, expectedAnatDataDir); assertEqual(anatImage, expectedFileName); + %% + opt.anatReference.session = '01'; + opt.anatReference.type = 'T1w'; + + [anatImage, anatDataDir] = getAnatFilename(BIDS, subLabel, opt); + + assertEqual(anatDataDir, expectedAnatDataDir); + assertEqual(anatImage, expectedFileName); + +end + +function test_getAnatFilenameTypeError() + + subLabel = '01'; + + opt = setOptions('vislocalizer', subLabel); + + opt.anatReference.type = 'T2w'; + + opt = checkOptions(opt); + + [BIDS, opt] = getData(opt); + + assertExceptionThrown( ... + @()getAnatFilename(BIDS, subLabel, opt), ... + 'getAnatFilename:requestedSuffixUnvailable'); + +end + +function test_getAnatFilenameSEssionError() + + subLabel = '01'; + + opt = setOptions('vislocalizer', subLabel); + + opt.anatReference.session = '001'; + + opt = checkOptions(opt); + + [BIDS, opt] = getData(opt); + + assertExceptionThrown( ... + @()getAnatFilename(BIDS, subLabel, opt), ... + 'getAnatFilename:requestedSessionUnvailable'); + end diff --git a/tests/test_getBoldFilename.m b/tests/test_getBoldFilename.m index be89963e..4a84d601 100644 --- a/tests/test_getBoldFilename.m +++ b/tests/test_getBoldFilename.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_getBoldFilename %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -8,25 +10,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); + + [BIDS, opt] = getData(opt); - [~, opt, BIDS] = getData(opt); + opt.query = struct('acq', ''); - sessions = getInfo(BIDS, subID, opt, 'Sessions'); + sessions = getInfo(BIDS, subLabel, opt, 'Sessions'); - runs = getInfo(BIDS, subID, opt, 'Runs', sessions{iSes}); + 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..a6bd762c 100644 --- a/tests/test_getBoldFilenameForFFX.m +++ b/tests/test_getBoldFilenameForFFX.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_getBoldFilenameForFFX %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -8,20 +10,16 @@ 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 = checkOptions(opt); + opt = setOptions('vislocalizer', subLabel); - [~, 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 7f4a07f4..7a2cbe68 100644 --- a/tests/test_getData.m +++ b/tests/test_getData.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_getData %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -6,64 +8,11 @@ initTestSuite; 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'}; +function test_getDataMetadata() - [group] = getData(opt); + subLabel = '01'; - 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 = {''}; - - opt.subjects = {'01'}; + opt = setOptions('vismotion', subLabel); [~, opt] = getData(opt, [], 'T1w'); @@ -72,34 +21,11 @@ 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'); assertExceptionThrown( ... @()getData(opt), ... '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 diff --git a/tests/test_getFFXdir.m b/tests/test_getFFXdir.m index ba269464..28734ca8 100644 --- a/tests/test_getFFXdir.m +++ b/tests/test_getFFXdir.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_getFFXdir %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -9,41 +11,33 @@ function test_getFFXdirBasic() funcFWFM = 0; - subID = '01'; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'funcLocalizer'; - - opt = setDerivativesDir(opt); + subLabel = '01'; - opt = checkOptions(opt); + opt = setOptions('vislocalizer', subLabel); expectedOutput = fullfile(fileparts(mfilename('fullpath')), 'dummyData', 'derivatives', ... - 'cpp_spm', 'sub-01', 'stats', 'ffx_task-funcLocalizer', ... - 'ffx_space-MNI_FWHM-0'); + 'cpp_spm-stats', 'sub-01', 'stats', ... + 'task-vislocalizer_space-MNI_FWHM-0'); - ffxDir = getFFXdir(subID, funcFWFM, opt); + ffxDir = getFFXdir(subLabel, funcFWFM, opt); assertEqual(exist(expectedOutput, 'dir'), 7); end -function test_getFFXdirMvpa() +function test_getFFXdirUserSpecified() - funcFWFM = 6; - subID = '02'; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'nBack'; - opt.space = 'individual'; + funcFWHM = 6; + subLabel = '02'; - opt = setDerivativesDir(opt); + opt = setOptions('nback', subLabel); + opt.space = 'individual'; - opt = checkOptions(opt); + ffxDir = getFFXdir(subLabel, funcFWHM, 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); + 'cpp_spm-stats', 'sub-02', 'stats', ... + 'task-nback_space-individual_FWHM-6_desc-nbackMVPA'); assertEqual(exist(expectedOutput, 'dir'), 7); diff --git a/tests/test_getInfo.m b/tests/test_getInfo.m deleted file mode 100644 index 837a276e..00000000 --- a/tests/test_getInfo.m +++ /dev/null @@ -1,64 +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() - % 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'); - opt.groups = {''}; - opt.subjects = {[], []}; - - %% Get sessions from BIDS - opt.taskName = 'vismotion'; - subID = 'ctrl01'; - info = 'sessions'; - [~, opt, BIDS] = getData(opt); - sessions = getInfo(BIDS, subID, opt, info); - assert(all(strcmp(sessions, {'01' '02'}))); - - %% Get runs from BIDS - opt.taskName = 'vismotion'; - subID = 'ctrl01'; - info = 'runs'; - session = '01'; - [~, 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'; - info = 'runs'; - session = '01'; - [~, opt, BIDS] = getData(opt); - runs = getInfo(BIDS, subID, opt, info, session); - assert(strcmp(runs, {''})); - - %% Get filename from BIDS - opt.taskName = 'vismotion'; - subID = 'ctrl01'; - session = '01'; - run = '1'; - info = 'filename'; - [~, opt, BIDS] = getData(opt); - 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)); - -end diff --git a/tests/test_getMeanFuncFilename.m b/tests/test_getMeanFuncFilename.m index d57159ad..5e7de78d 100644 --- a/tests/test_getMeanFuncFilename.m +++ b/tests/test_getMeanFuncFilename.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_getMeanFuncFilename %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -8,18 +10,13 @@ function test_getMeanFuncFilenameBasic() - subID = '01'; - - opt.taskName = 'vislocalizer'; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.groups = {''}; - opt.subjects = {subID}; + subLabel = '01'; - opt = checkOptions(opt); + opt = setOptions('vislocalizer', subLabel); - [~, 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_getRFXdir.m b/tests/test_getRFXdir.m index 3473ae94..2fb709d1 100644 --- a/tests/test_getRFXdir.m +++ b/tests/test_getRFXdir.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_getRFXdir %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -11,10 +13,28 @@ function test_getRFXdirBasic() funcFWHM = 0; conFWHM = 0; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'funcLocalizer'; + opt = setOptions('vislocalizer'); + + rfxDir = getRFXdir(opt, funcFWHM, conFWHM); + + expectedOutput = fullfile( ... + fileparts(mfilename('fullpath')), ... + 'dummyData', ... + 'derivatives', ... + 'cpp_spm-stats', ... + 'group', ... + 'task-vislocalizer_space-MNI_FWHM-0_conFWHM-0'); + + assertEqual(exist(expectedOutput, 'dir'), 7); + +end + +function test_getFFXdirUserSpecified() + + conFWHM = 0; + funcFWHM = 6; - opt = setDerivativesDir(opt); + opt = setOptions('nback'); rfxDir = getRFXdir(opt, funcFWHM, conFWHM); @@ -22,10 +42,9 @@ function test_getRFXdirBasic() fileparts(mfilename('fullpath')), ... 'dummyData', ... 'derivatives', ... - 'cpp_spm', ... + 'cpp_spm-stats', ... 'group', ... - 'rfx_task-funcLocalizer', ... - 'rfx_funcFWHM-0_conFWHM-0'); + 'task-nback_space-MNI_FWHM-6_conFWHM-0_desc-nbackMVPA'); assertEqual(exist(expectedOutput, 'dir'), 7); diff --git a/tests/test_getRealignParamFile.m b/tests/test_getRealignParamFile.m index e4e66f66..2c4fd28e 100644 --- a/tests/test_getRealignParamFile.m +++ b/tests/test_getRealignParamFile.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_getRealignParamFile %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -8,19 +10,15 @@ function test_getRealignParamFileBasic() - subID = '01'; + subLabel = '01'; session = '01'; run = ''; - opt.taskName = 'vislocalizer'; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.subjects = {subID}; - - opt = checkOptions(opt); + opt = setOptions('vislocalizer', subLabel); - [~, 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 +32,16 @@ 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); + [BIDS, opt] = getData(opt); - [~, opt, BIDS] = 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,16 @@ 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 = checkOptions(opt); + opt = setOptions('vislocalizer', subLabel); - [~, 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_loadAndCheckOptions.m b/tests/test_loadAndCheckOptions.m index 7da32e0a..624063e8 100644 --- a/tests/test_loadAndCheckOptions.m +++ b/tests/test_loadAndCheckOptions.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_loadAndCheckOptions %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -8,18 +10,18 @@ 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 opt = loadAndCheckOptions(); - expectedOptions = defaultOptions(); - expectedOptions.taskName = 'vismotion'; + expectedOptions = defaultOptions('vismotion'); assertEqual(opt, expectedOptions); @@ -27,14 +29,16 @@ function test_loadAndCheckOptionsBasic() function test_loadAndCheckOptionsStructure() + mkdir cfg; + delete(fullfile(pwd, 'cfg', '*.json')); + % create dummy json file opt.taskName = 'vismotion'; % makes sure that it is picked up by default opt = loadAndCheckOptions(opt); - expectedOptions = defaultOptions(); - expectedOptions.taskName = 'vismotion'; + expectedOptions = defaultOptions('vismotion'); assertEqual(opt, expectedOptions); @@ -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,14 +55,13 @@ 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'; + expectedOptions = defaultOptions('vismotion'); expectedOptions.space = 'individual'; assertEqual(opt, expectedOptions); @@ -68,37 +72,37 @@ 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 opt = loadAndCheckOptions(); - expectedOptions = defaultOptions(); - expectedOptions.taskName = 'vismotion'; + expectedOptions = defaultOptions('vismotion'); expectedOptions.space = 'individual'; expectedOptions.funcVoxelDims = [1 1 1]'; @@ -106,43 +110,13 @@ function test_loadAndCheckOptionsFromSeveralFiles() 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]; +function test_loadAndCheckOptionsMoAE() - expectedOptions.result.Steps = returnDefaultResultsStructure(); + jsonContent = setOptions('MoAE'); - expectedOptions.parallelize.do = false; - expectedOptions.parallelize.nbWorkers = 3; - expectedOptions.parallelize.killOnExit = true; + optionJsonFile = fullfile(pwd, 'cfg', 'options_task-auditory.json'); + spm_jsonwrite(optionJsonFile, jsonContent); - expectedOptions = orderfields(expectedOptions); + opt = loadAndCheckOptions(optionJsonFile); end diff --git a/tests/test_modelFiles.m b/tests/test_modelFiles.m index 4c442b60..6024012b 100644 --- a/tests/test_modelFiles.m +++ b/tests/test_modelFiles.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM 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..94d956a8 100644 --- a/tests/test_saveMatlabBatch.m +++ b/tests/test_saveMatlabBatch.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM 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..cd21d71f 100644 --- a/tests/test_setBatch3Dto4D.m +++ b/tests/test_setBatch3Dto4D.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM 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..0efe5e5e 100644 --- a/tests/test_setBatchComputeVDM.m +++ b/tests/test_setBatchComputeVDM.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM 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 fe07cb59..a789212e 100644 --- a/tests/test_setBatchCoregistrationFuncToAnat.m +++ b/tests/test_setBatchCoregistrationFuncToAnat.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_setBatchCoregistrationFuncToAnat %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -11,20 +13,17 @@ function test_setBatchCoregistrationFuncToAnatBasic() % necessarry to deal with SPM module dependencies spm_jobman('initcfg'); - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'vismotion'; - - opt = checkOptions(opt); + subLabel = '02'; - [~, opt, BIDS] = getData(opt); + opt = setOptions('vismotion', subLabel); - 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 +48,18 @@ 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 = checkOptions(opt); - - [~, opt, BIDS] = getData(opt); + opt = setOptions('vismotion', subLabel); + opt.realign.useUnwarp = false; - 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..e5fcd5ba 100644 --- a/tests/test_setBatchFactorialDesign.m +++ b/tests/test_setBatchFactorialDesign.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_setBatchFactorialDesign %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -11,13 +13,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'); - - opt = checkOptions(opt); matlabbatch = []; matlabbatch = setBatchFactorialDesign(matlabbatch, opt, funcFWHM, conFWHM); 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 diff --git a/tests/test_setBatchImageCalculation.m b/tests/test_setBatchImageCalculation.m index 235d27a4..0e70d94c 100644 --- a/tests/test_setBatchImageCalculation.m +++ b/tests/test_setBatchImageCalculation.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM 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 780567c4..6b15f9fd 100644 --- a/tests/test_setBatchMeanAnatAndMask.m +++ b/tests/test_setBatchMeanAnatAndMask.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_setBatchMeanAnatAndMask %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -10,49 +12,46 @@ 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 = checkOptions(opt); matlabbatch = []; 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', ... - 'ffx_task-vismotion', ... - 'ffx_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'); - - 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.dir.stats, 'sub-01', ... + 'stats', ... + 'task-vismotion_space-MNI_FWHM-6', ... + 'mask.nii'); + imcalc.input{2, 1} = fullfile(opt.dir.stats, ... + '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_setBatchNormalizationSpatialPrepro.m b/tests/test_setBatchNormalizationSpatialPrepro.m index 7a3199a5..cb505861 100644 --- a/tests/test_setBatchNormalizationSpatialPrepro.m +++ b/tests/test_setBatchNormalizationSpatialPrepro.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_setBatchNormalizationSpatialPrepro %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -13,6 +15,7 @@ function test_setBatchNormalizationSpatialPreproBasic() opt.orderBatches.coregister = 3; opt.orderBatches.segment = 5; + opt.orderBatches.skullStripping = 6; matlabbatch = {}; voxDim = [3 3 3]; @@ -20,7 +23,10 @@ function test_setBatchNormalizationSpatialPreproBasic() expectedBatch = returnExpectedBatch(voxDim); - assertEqual(expectedBatch, matlabbatch); + % assertEqual(matlabbatch{end}.spm.spatial.normalise.write.subj, ... + % expectedBatch{end}.spm.spatial.normalise.write.subj); + + assertEqual(matlabbatch, expectedBatch); end @@ -30,7 +36,7 @@ function test_setBatchNormalizationSpatialPreproBasic() jobsToAdd = numel(expectedBatch) + 1; - for iJob = jobsToAdd:(jobsToAdd + 4) + for iJob = jobsToAdd:(jobsToAdd + 5) expectedBatch{iJob}.spm.spatial.normalise.write.subj.def(1) = ... cfg_dep('Segment: Forward Deformations', ... substruct( ... @@ -95,4 +101,13 @@ function test_setBatchNormalizationSpatialPreproBasic() '.', 'c', '()', {':'})); expectedBatch{jobsToAdd + 4}.spm.spatial.normalise.write.woptions.vox = voxDim; + expectedBatch{jobsToAdd + 5}.spm.spatial.normalise.write.subj.resample(1) = ... + cfg_dep('Image Calculator: skullstripped anatomical', ... + substruct( ... + '.', 'val', '{}', {6}, ... + '.', 'val', '{}', {1}, ... + '.', 'val', '{}', {1}), ... + substruct('.', 'files')); + expectedBatch{jobsToAdd + 5}.spm.spatial.normalise.write.woptions.vox = [1 1 1]; + end diff --git a/tests/test_setBatchRealign.m b/tests/test_setBatchRealign.m index 5be5d90f..cc48d55c 100644 --- a/tests/test_setBatchRealign.m +++ b/tests/test_setBatchRealign.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_setBatchRealign %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -14,18 +16,13 @@ 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'; - - opt = checkOptions(opt); - - [~, opt, BIDS] = getData(opt); + subLabel = '01'; - subID = '01'; + opt = setOptions('MoAE', subLabel); + [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 +30,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_setBatchResults.m b/tests/test_setBatchResults.m index ca500734..490577ae 100644 --- a/tests/test_setBatchResults.m +++ b/tests/test_setBatchResults.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_setBatchResults %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -16,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); @@ -30,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; @@ -49,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; @@ -64,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'; @@ -128,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 diff --git a/tests/test_setBatchSTC.m b/tests/test_setBatchSTC.m index 10dda843..85817da4 100644 --- a/tests/test_setBatchSTC.m +++ b/tests/test_setBatchSTC.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_setBatchSTC %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -8,16 +10,14 @@ function test_setBatchSTCEmpty() - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'vislocalizer'; + subLabel = '02'; - opt = checkOptions(opt); + opt = setOptions('vislocalizer', subLabel); - [~, 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 +26,10 @@ 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 +37,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 +48,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 +62,15 @@ function test_setBatchSTCForce() function test_setBatchSTCBasic() - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'vismotion'; - - opt = checkOptions(opt); + subLabel = '02'; - [~, opt, BIDS] = getData(opt); + opt = setOptions('vismotion', subLabel); + opt.query = struct('acq', ''); - subID = '02'; + [BIDS, opt] = getData(opt); matlabbatch = []; - matlabbatch = setBatchSTC(matlabbatch, BIDS, opt, subID); + matlabbatch = setBatchSTC(matlabbatch, BIDS, opt, subLabel); TR = 1.5; sliceOrder = repmat([ ... @@ -85,11 +83,12 @@ function test_setBatchSTCBasic() runCounter = 1; for iSes = 1:2 - fileName = spm_BIDS(BIDS, 'data', ... - 'sub', subID, ... - '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', ... + 'acq', ''); expectedBatch{1}.spm.temporal.st.scans{runCounter} = ... {fileName{1}}; expectedBatch{1}.spm.temporal.st.scans{runCounter + 1} = ... @@ -103,23 +102,20 @@ 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) = []; opt.STC_referenceSlice = 2; % impossible reference value - 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 @@ -128,6 +124,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; diff --git a/tests/test_setBatchSaveCoregistrationMatrix.m b/tests/test_setBatchSaveCoregistrationMatrix.m index 89889ce1..b21575b0 100644 --- a/tests/test_setBatchSaveCoregistrationMatrix.m +++ b/tests/test_setBatchSaveCoregistrationMatrix.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_setBatchSaveCoregistrationMatrix %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -11,18 +13,17 @@ 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 = checkOptions(opt); + opt = setOptions('vismotion', subLabel); + opt.query = struct('acq', ''); - [~, 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..91872c4d 100644 --- a/tests/test_setBatchSegmentation.m +++ b/tests/test_setBatchSegmentation.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_setBatchSegmentation %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -60,17 +62,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..083a9c32 100644 --- a/tests/test_setBatchSelectAnat.m +++ b/tests/test_setBatchSelectAnat.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_setBatchSelectAnat %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -12,18 +14,14 @@ 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'; - - opt = checkOptions(opt); + subLabel = '01'; - [~, opt, BIDS] = getData(opt); + opt = setOptions('MoAE', subLabel); - 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..921c227f 100644 --- a/tests/test_setBatchSkullStripping.m +++ b/tests/test_setBatchSkullStripping.m @@ -1,4 +1,4 @@ -% (C) Copyright 2019 CPP BIDS SPM-pipeline developers +% (C) Copyright 2019 CPP_SPM developers function test_suite = test_setBatchSkullStripping %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 @@ -10,21 +10,16 @@ function test_setBatchSkullStrippingBasic() - 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); 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..98357b9c 100644 --- a/tests/test_setBatchSmoothConImages.m +++ b/tests/test_setBatchSmoothConImages.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_setBatchSmoothConImages %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -8,61 +10,47 @@ 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 = checkOptions(opt); - [group, opt] = getData(opt); + opt = setOptions('vismotion'); + opt.subjects = {'01', '02'}; + + [~, 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'; - expectedBatch{1}.spm.spatial.smooth.data = {fullfile(opt.derivativesDir, 'sub-01', 'stats', ... - 'ffx_task-vismotion', ... - 'ffx_space-MNI_FWHM-6', ... + expectedBatch{1}.spm.spatial.smooth.data = {fullfile(opt.dir.stats, 'sub-01', 'stats', ... + 'task-vismotion_space-MNI_FWHM-6', ... 'con_0001.nii'); ... - fullfile(opt.derivativesDir, 'sub-01', 'stats', ... - 'ffx_task-vismotion', ... - 'ffx_space-MNI_FWHM-6', ... + fullfile(opt.dir.stats, 'sub-01', 'stats', ... + 'task-vismotion_space-MNI_FWHM-6', ... 'con_0002.nii'); ... - fullfile(opt.derivativesDir, 'sub-01', 'stats', ... - 'ffx_task-vismotion', ... - 'ffx_space-MNI_FWHM-6', ... + fullfile(opt.dir.stats, 'sub-01', 'stats', ... + 'task-vismotion_space-MNI_FWHM-6', ... 'con_0003.nii'); ... - fullfile(opt.derivativesDir, 'sub-01', 'stats', ... - 'ffx_task-vismotion', ... - 'ffx_space-MNI_FWHM-6', ... + fullfile(opt.dir.stats, 'sub-01', 'stats', ... + 'task-vismotion_space-MNI_FWHM-6', ... 'con_0004.nii')}; expectedBatch{1}.spm.spatial.smooth.dtype = 0; expectedBatch{1}.spm.spatial.smooth.im = 0; 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', ... + expectedBatch{2}.spm.spatial.smooth.data = {fullfile(opt.dir.stats, 'sub-02', 'stats', ... + 'task-vismotion_space-MNI_FWHM-6', ... 'con_0001.nii'); ... - fullfile(opt.derivativesDir, 'sub-02', 'stats', ... - 'ffx_task-vismotion', ... - 'ffx_space-MNI_FWHM-6', ... + fullfile(opt.dir.stats, 'sub-02', 'stats', ... + 'task-vismotion_space-MNI_FWHM-6', ... 'con_0002.nii'); ... - fullfile(opt.derivativesDir, 'sub-02', 'stats', ... - 'ffx_task-vismotion', ... - 'ffx_space-MNI_FWHM-6', ... + fullfile(opt.dir.stats, 'sub-02', 'stats', ... + 'task-vismotion_space-MNI_FWHM-6', ... 'con_0003.nii'); ... - fullfile(opt.derivativesDir, 'sub-02', 'stats', ... - 'ffx_task-vismotion', ... - 'ffx_space-MNI_FWHM-6', ... + fullfile(opt.dir.stats, 'sub-02', 'stats', ... + '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_setBatchSmoothing.m b/tests/test_setBatchSmoothing.m index 982219e3..f1016267 100644 --- a/tests/test_setBatchSmoothing.m +++ b/tests/test_setBatchSmoothing.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM 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 0ea2ea31..564f93ed 100644 --- a/tests/test_setBatchSmoothingFunc.m +++ b/tests/test_setBatchSmoothingFunc.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_setBatchSmoothingFunc %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -11,21 +13,17 @@ 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 = checkOptions(opt); + opt = setOptions('MoAE', subLabel); - [~, 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 +35,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..e2d3c548 100644 --- a/tests/test_setBatchSubjectLevelContrasts.m +++ b/tests/test_setBatchSubjectLevelContrasts.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_setBatchSubjectLevelContrasts %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -8,30 +10,19 @@ function test_setBatchSubjectLevelContrastsBasic() - subID = '01'; + subLabel = '01'; funcFWHM = 6; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - 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); + opt = setOptions('vismotion', subLabel); matlabbatch = []; - matlabbatch = setBatchSubjectLevelContrasts(matlabbatch, opt, subID, funcFWHM); + matlabbatch = setBatchSubjectLevelContrasts(matlabbatch, opt, subLabel, funcFWHM); expectedBatch = []; - expectedBatch{end + 1}.spm.stats.con.spmmat = {fullfile(opt.derivativesDir, ... + expectedBatch{end + 1}.spm.stats.con.spmmat = {fullfile(opt.dir.stats, ... '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_setBatchSubjectLevelGLMSpec.m b/tests/test_setBatchSubjectLevelGLMSpec.m index d2a13cd2..94347c23 100644 --- a/tests/test_setBatchSubjectLevelGLMSpec.m +++ b/tests/test_setBatchSubjectLevelGLMSpec.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_setBatchSubjectLevelGLMSpec %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -9,29 +11,22 @@ 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 = checkOptions(opt); + opt = setOptions('MoAE', subLabel); 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 +35,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..6ca6d728 100644 --- a/tests/test_setBatchSubjectLevelResults.m +++ b/tests/test_setBatchSubjectLevelResults.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM developers + function test_suite = test_setBatchSubjectLevelResults %#ok<*STOUT> try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -11,28 +13,23 @@ 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 = {}; - expectedBatch{end + 1}.spm.stats.results.spmmat = {fullfile(opt.derivativesDir, ... + expectedBatch{end + 1}.spm.stats.results.spmmat = {fullfile(opt.dir.stats, ... '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'; @@ -56,21 +53,17 @@ 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); matlabbatch = []; assertExceptionThrown( ... @()setBatchSubjectLevelResults(matlabbatch, ... opt, ... - subID, ... + subLabel, ... funcFWHM, ... iStep, ... iCon), ... @@ -83,23 +76,22 @@ function test_setBatchSubjectLevelResultsErrorNoMAtchingContrast() iStep = 1; iCon = 1; - subID = '01'; + subLabel = '01'; funcFWHM = 6; - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); + subLabel = '01'; + funcFWHM = 6; + + opt = setOptions('vismotion', subLabel); opt.space = 'MNI'; - opt.taskName = 'vismotion'; opt.result.Steps.Contrasts.Name = 'NotAContrast'; - opt = setDerivativesDir(opt); - opt = checkOptions(opt); - matlabbatch = []; assertExceptionThrown( ... @()setBatchSubjectLevelResults(matlabbatch, ... opt, ... - subID, ... + subLabel, ... funcFWHM, ... iStep, ... iCon), ... diff --git a/tests/test_setDefaultFields.m b/tests/test_setFields.m similarity index 63% rename from tests/test_setDefaultFields.m rename to tests/test_setFields.m index 548101ae..04412fd3 100644 --- a/tests/test_setDefaultFields.m +++ b/tests/test_setFields.m @@ -1,4 +1,6 @@ -function test_suite = test_setDefaultFields %#ok<*STOUT> +% (C) Copyright 2020 CPP_SPM developers + +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 @@ -6,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; @@ -23,7 +25,7 @@ function test_setDefaultFieldsWrite() end -function test_setDefaultFieldsNoOverwrite() +function test_setFieldsNoOverwrite() % set up structure.field.subfield_1 = 3; @@ -31,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; @@ -42,7 +44,28 @@ function test_setDefaultFieldsNoOverwrite() end -function test_setDefaultFieldsCmplxStruct() +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 structure = struct(); @@ -53,7 +76,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; diff --git a/tests/test_cleanCrash.m b/tests/test_unit_cleanCrash.m similarity index 83% rename from tests/test_cleanCrash.m rename to tests/test_unit_cleanCrash.m index d1781f88..55deacc7 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_SPM 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 94% rename from tests/test_copyGraphWindownOutput.m rename to tests/test_unit_copyGraphWindownOutput.m index 0958cc5a..27d603e3 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_SPM 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 65% rename from tests/test_createDataDictionary.m rename to tests/test_unit_createDataDictionary.m index 1ee7ceb5..f9bbe888 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_SPM 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 @@ -8,24 +10,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); + + [BIDS, opt] = getData(opt); - [~, opt, BIDS] = getData(opt); + opt.query = struct('acq', ''); - sessions = getInfo(BIDS, subID, opt, 'Sessions'); + sessions = getInfo(BIDS, subLabel, opt, 'Sessions'); - runs = getInfo(BIDS, subID, opt, 'Runs', sessions{iSes}); + 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_unit_createGlmDirName.m b/tests/test_unit_createGlmDirName.m new file mode 100644 index 00000000..67765e2d --- /dev/null +++ b/tests/test_unit_createGlmDirName.m @@ -0,0 +1,23 @@ +% (C) Copyright 2021 CPP_SPM 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 diff --git a/tests/test_getFuncVoxelDims.m b/tests/test_unit_getFuncVoxelDims.m similarity index 86% rename from tests/test_getFuncVoxelDims.m rename to tests/test_unit_getFuncVoxelDims.m index f7800da6..929e0195 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_SPM 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 @@ -11,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/test_getGrpLevelContrastToCompute.m b/tests/test_unit_getGrpLevelContrastToCompute.m similarity index 85% rename from tests/test_getGrpLevelContrastToCompute.m rename to tests/test_unit_getGrpLevelContrastToCompute.m index 6ac688d0..368ca7a8 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_SPM 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 @@ -9,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_getInfo.m b/tests/test_unit_getInfo.m new file mode 100644 index 00000000..6e948881 --- /dev/null +++ b/tests/test_unit_getInfo.m @@ -0,0 +1,113 @@ +% (C) Copyright 2020 CPP_SPM developers + +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() + + subLabel = 'ctrl01'; + + opt = setOptions('vismotion', subLabel); + + 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(strcmp(runs, {''})); + +end + +function test_getInfoQuery() + + subLabel = 'ctrl01'; + + session = '01'; + run = '1'; + info = 'filename'; + + opt = setOptions('vismotion', subLabel); + + [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', ... + ['sub-' subLabel], ['ses-' session], 'func', ... + ['sub-' subLabel, ... + '_ses-' session, ... + '_task-' opt.taskName, ... + '_run-' run, ... + '_bold.nii']); + + assert(strcmp(filename, 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('vismotion', subLabel); + + [BIDS, opt] = getData(opt); + + opt.query = struct('ses', {{'01', '02'}}); + [~, nbSessions] = getInfo(BIDS, subLabel, opt, 'sessions'); + assertEqual(nbSessions, numel(opt.query.ses)); + + 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/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 dfbdeccb..619a9e91 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_SPM 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 @@ -10,6 +12,7 @@ function test_getPrefixSTC() step = 'realign'; funcFWHM = 6; + opt.metadata.SliceTiming = 1:0.2:1.8; opt.sliceOrder = 1:10; @@ -26,6 +29,7 @@ function test_getPrefixSTC() function test_getPrefixNoSTC() step = 'realign'; + opt.metadata = []; opt.sliceOrder = []; diff --git a/tests/test_getSliceOrder.m b/tests/test_unit_getSliceOrder.m similarity index 68% rename from tests/test_getSliceOrder.m rename to tests/test_unit_getSliceOrder.m index 25e06a14..543c141e 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_SPM 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 @@ -7,12 +9,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 = checkOptions(opt); + opt = setOptions('vismotion'); [~, opt] = getData(opt); BIDS_sliceOrder = getSliceOrder(opt, 0); @@ -40,11 +38,7 @@ function test_getSliceOrderBasic() function test_getSliceOrderEmpty() - %% Get empty slice order from BIDS - opt.derivativesDir = fullfile(fileparts(mfilename('fullpath')), 'dummyData'); - opt.taskName = 'vislocalizer'; - - opt = checkOptions(opt); + opt = setOptions('vislocalizer'); [~, opt] = getData(opt); @@ -56,13 +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); BIDS_sliceOrder = getSliceOrder(opt, 0); diff --git a/tests/test_unit_getSubjectList.m b/tests/test_unit_getSubjectList.m new file mode 100644 index 00000000..fd9a0c8e --- /dev/null +++ b/tests/test_unit_getSubjectList.m @@ -0,0 +1,68 @@ +% (C) Copyright 2020 CPP_SPM 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 + end + initTestSuite; +end + +function test_getSubjectListNone() + + opt = setOptions('vismotion'); + + 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('vismotion'); + + 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('vismotion'); + + 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('vismotion', '03'); + + BIDS = bids.layout(opt.derivativesDir); + + assertExceptionThrown( ... + @()getSubjectList(BIDS, opt), ... + 'getSubjectList:noMatchingSubject'); + +end diff --git a/tests/test_manageWorkersPool.m b/tests/test_unit_manageWorkersPool.m similarity index 92% rename from tests/test_manageWorkersPool.m rename to tests/test_unit_manageWorkersPool.m index 9754a9ea..159ae73a 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_SPM 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 88% rename from tests/test_returnDefaultResultsStructure.m rename to tests/test_unit_returnDefaultResultsStructure.m index 75cd688c..6e7df827 100644 --- a/tests/test_returnDefaultResultsStructure.m +++ b/tests/test_unit_returnDefaultResultsStructure.m @@ -1,6 +1,6 @@ -% (C) Copyright 2020 CPP BIDS SPM-pipeline developers +% (C) Copyright 2020 CPP_SPM 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 84% rename from tests/test_returnEmptyModel.m rename to tests/test_unit_returnEmptyModel.m index 343ee25b..2b8525bb 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_SPM 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 92% rename from tests/test_setDerivativesDir.m rename to tests/test_unit_setDerivativesDir.m index 4afab5cb..6cf46f8c 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_SPM 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 66% rename from tests/test_specifyContrasts.m rename to tests/test_unit_specifyContrasts.m index 5c3ff220..01332c50 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_SPM 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 @@ -9,19 +11,12 @@ 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.model.file = ... - fullfile(fileparts(mfilename('fullpath')), ... - 'dummyData', 'models', 'model-visMotionLoc_smdl.json'); - - opt = setDerivativesDir(opt); + opt = setOptions('vismotion', subLabel); - ffxDir = getFFXdir(subID, funcFWFM, opt); + ffxDir = getFFXdir(subLabel, funcFWFM, opt); contrasts = specifyContrasts(ffxDir, opt.taskName, opt); diff --git a/tests/test_validationInputFile.m b/tests/test_unit_validationInputFile.m similarity index 91% rename from tests/test_validationInputFile.m rename to tests/test_unit_validationInputFile.m index b72e8413..d76c0537 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_SPM 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..38efe2b1 100644 --- a/tests/test_utils.m +++ b/tests/test_utils.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM 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..af922521 100644 --- a/tests/test_writeDatasetDescription.m +++ b/tests/test_writeDatasetDescription.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_SPM 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/defaultOptions.m b/tests/utils/defaultOptions.m new file mode 100644 index 00000000..d258de43 --- /dev/null +++ b/tests/utils/defaultOptions.m @@ -0,0 +1,56 @@ +function expectedOptions = defaultOptions(taskName) + % + % (C) Copyright 2021 CPP_SPM developers + + expectedOptions.sliceOrder = []; + expectedOptions.STC_referenceSlice = []; + + expectedOptions.dataDir = ''; + expectedOptions.derivativesDir = ''; + expectedOptions.dir = struct('raw', '', ... + 'derivatives', ''); + + 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.glm.QA.do = true; + expectedOptions.glm.roibased.do = false; + + expectedOptions.model.file = ''; + expectedOptions.model.hrfDerivatives = [0 0]; + + expectedOptions.result.Steps = returnDefaultResultsStructure(); + + expectedOptions.parallelize.do = false; + expectedOptions.parallelize.nbWorkers = 3; + expectedOptions.parallelize.killOnExit = true; + + if nargin > 0 + expectedOptions.taskName = taskName; + end + + expectedOptions = orderfields(expectedOptions); + + expectedOptions = setStatsDir(expectedOptions); + +end diff --git a/tests/utils/setOptions.m b/tests/utils/setOptions.m new file mode 100644 index 00000000..6717eb29 --- /dev/null +++ b/tests/utils/setOptions.m @@ -0,0 +1,36 @@ +function opt = setOptions(task, subLabel) + % + % (C) Copyright 2021 CPP_SPM developers + + thisDir = fileparts(mfilename('fullpath')); + + opt.dir = []; + + if strcmp(task, 'MoAE') + + opt.dataDir = fullfile(thisDir, ... + '..', '..', 'demos', 'MoAE', 'inputs', 'raw'); + opt.model.file = fullfile(thisDir, ... + '..', '..', 'demos', 'MoAE', 'models', 'model-MoAE_smdl.json'); + + opt.taskName = 'auditory'; + + opt.result.Steps.Contrasts(1).Name = 'listening'; + opt.result.Steps.Contrasts(2).Name = 'listening_inf_baseline'; + + else + + opt.taskName = task; + opt.derivativesDir = fullfile(thisDir, '..', 'dummyData'); + opt.model.file = fullfile(opt.derivativesDir, 'models', ... + ['model-' task '_smdl.json']); + + end + + opt = checkOptions(opt); + + if nargin > 1 + opt.subjects = {subLabel}; + end + +end diff --git a/version.txt b/version.txt index 81fd7ba0..60453e69 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v0.2.0 \ No newline at end of file +v1.0.0 \ No newline at end of file