diff --git a/+bids/+internal/add_missing_field.m b/+bids/+internal/add_missing_field.m index 21ff44c7..a5f96ec1 100644 --- a/+bids/+internal/add_missing_field.m +++ b/+bids/+internal/add_missing_field.m @@ -4,6 +4,7 @@ % % structure = add_missing_field(structure, field) % + % (C) Copyright 2021 BIDS-MATLAB developers if ~isfield(structure, field) diff --git a/+bids/+internal/append_to_layout.m b/+bids/+internal/append_to_layout.m index 4556082d..d1ed62ee 100644 --- a/+bids/+internal/append_to_layout.m +++ b/+bids/+internal/append_to_layout.m @@ -16,6 +16,7 @@ % :type schema: structure % % + % (C) Copyright 2021 BIDS-MATLAB developers pth = [subject.path, filesep, modality]; @@ -156,7 +157,7 @@ function [msg, msg_id] = error_message(msg_id, file, extra) - msg = sprintf('Skipping file: %s.\n', file); + msg = sprintf('Skipping file: %s.\n', bids.internal.format_path(file)); switch msg_id diff --git a/+bids/+internal/camel_case.m b/+bids/+internal/camel_case.m index 40697487..0b772ab4 100644 --- a/+bids/+internal/camel_case.m +++ b/+bids/+internal/camel_case.m @@ -15,6 +15,7 @@ % for all words but the first one (``camelCase``) and % removes invalid characters (like spaces). % + % (C) Copyright 2018 BIDS-MATLAB developers % camel case: upper case for first letter for all words but the first one diff --git a/+bids/+internal/create_unordered_list.m b/+bids/+internal/create_unordered_list.m new file mode 100644 index 00000000..e8b5a4c2 --- /dev/null +++ b/+bids/+internal/create_unordered_list.m @@ -0,0 +1,61 @@ +function list = create_unordered_list(list) + % + % turns a cell string or a structure into a string + % that is an unordered list to print to the screen + % + % USAGE:: + % + % list = create_unordered_list(list) + % + % :param list: obligatory argument. + % :type list: cell string or structure + % + % + + % (C) Copyright 2022 Remi Gau + + if bids.internal.is_octave + warning('off', 'Octave:mixed-string-concat'); + end + + prefix = '\n\t- '; + + if ischar(list) + list = cellstr(list); + end + + if iscell(list) + + for i = 1:numel(list) + if isnumeric(list{i}) + list{i} = num2str(list{i}); + end + end + + list = sprintf([prefix, strjoin(list, prefix), '\n']); + + elseif isstruct(list) + + output = ''; + fields = fieldnames(list); + + for i = 1:numel(fields) + content = list.(fields{i}); + if ~iscell(content) + content = {content}; + end + + for j = 1:numel(content) + if isnumeric(content{j}) + content{j} = num2str(content{j}); + end + end + + output = [output prefix fields{i} ': {' strjoin(content, ', ') '}']; + end + + list = sprintf(output); + + end + +end diff --git a/+bids/+internal/download.m b/+bids/+internal/download.m index fe91ae6e..a49b2355 100644 --- a/+bids/+internal/download.m +++ b/+bids/+internal/download.m @@ -4,6 +4,7 @@ % % filename = download(URL, output_dir, verbose) % + % (C) Copyright 2021 BIDS-MATLAB developers if nargin < 2 output_dir = pwd; @@ -24,10 +25,14 @@ if strcmp(protocol, 'http:') if isunix() - if verbose - system(sprintf('wget %s', URL)); - else - system(sprintf('wget -q %s', URL)); + try + if verbose + system(sprintf('wget %s', URL)); + else + system(sprintf('wget -q %s', URL)); + end + catch + urlwrite(URL, filename); %#ok<*URLWR> end else urlwrite(URL, filename); @@ -35,7 +40,9 @@ % move file in case it was not downloaded in the root dir if ~exist(fullfile(output_dir, filename), 'file') - print_to_screen([filename ' --> ' output_dir], verbose); + print_to_screen([bids.internal.format_path(filename), ... + ' --> ', ... + bids.internal.format_path(output_dir)], verbose); movefile(filename, fullfile(output_dir, filename)); end filename = fullfile(output_dir, filename); diff --git a/+bids/+internal/ends_with.m b/+bids/+internal/ends_with.m index 529570fc..8c24c868 100644 --- a/+bids/+internal/ends_with.m +++ b/+bids/+internal/ends_with.m @@ -14,8 +14,10 @@ % % Based on the equivalent function from SPM12 % + % (C) Copyright 2011-2018 Guillaume Flandin, Wellcome Centre for Human Neuroimaging % + % (C) Copyright 2018 BIDS-MATLAB developers res = false; diff --git a/+bids/+internal/error_handling.m b/+bids/+internal/error_handling.m index 48d2218c..a34d92cd 100644 --- a/+bids/+internal/error_handling.m +++ b/+bids/+internal/error_handling.m @@ -6,15 +6,24 @@ function error_handling(varargin) % % :param function_name: default = ``bidsMatlab`` % :type function_name: + % % :param id: default = ``unspecified`` % :type id: string + % % :param msg: default = ``unspecified`` % :type msg: string + % % :param tolerant: - % :type tolerant: boolean + % :type tolerant: logical + % % :param verbose: - % :type verbose: boolean + % :type verbose: logical % + % EXAMPLE:: + % + % bids.internal.error_handling(mfilename(), 'thisError', 'this is an error', tolerant, verbose) + % + % (C) Copyright 2018 BIDS-MATLAB developers default_function_name = 'bidsMatlab'; @@ -23,28 +32,28 @@ function error_handling(varargin) default_tolerant = true; default_verbose = false; - p = inputParser; + args = inputParser; - addOptional(p, 'function_name', default_function_name, @ischar); - addOptional(p, 'id', default_id, @ischar); - addOptional(p, 'msg', default_msg, @ischar); - addOptional(p, 'tolerant', default_tolerant, @islogical); - addOptional(p, 'verbose', default_verbose, @islogical); + addOptional(args, 'function_name', default_function_name, @ischar); + addOptional(args, 'id', default_id, @ischar); + addOptional(args, 'msg', default_msg, @ischar); + addOptional(args, 'tolerant', default_tolerant, @islogical); + addOptional(args, 'verbose', default_verbose, @islogical); - parse(p, varargin{:}); + parse(args, varargin{:}); - function_name = bids.internal.file_utils(p.Results.function_name, 'basename'); + function_name = bids.internal.file_utils(args.Results.function_name, 'basename'); - id = [function_name, ':' p.Results.id]; - msg = p.Results.msg; + id = [function_name, ':' args.Results.id]; + msg = sprintf(['\n' args.Results.msg '\n']); - if ~p.Results.tolerant + if ~args.Results.tolerant errorStruct.identifier = id; errorStruct.message = msg; error(errorStruct); end - if p.Results.verbose + if args.Results.verbose warning(id, msg); end diff --git a/+bids/+internal/file_utils.m b/+bids/+internal/file_utils.m index 5b1951af..2f2cedf2 100644 --- a/+bids/+internal/file_utils.m +++ b/+bids/+internal/file_utils.m @@ -36,8 +36,10 @@ % % Based on spm_file.m and spm_select.m from SPM12. % + % (C) Copyright 2011-2018 Guillaume Flandin, Wellcome Centre for Human Neuroimaging % + % (C) Copyright 2018 BIDS-MATLAB developers %#ok<*AGROW> diff --git a/+bids/+internal/format_path.m b/+bids/+internal/format_path.m new file mode 100644 index 00000000..b6c339dd --- /dev/null +++ b/+bids/+internal/format_path.m @@ -0,0 +1,33 @@ +function pth = format_path(pth) + % + % + % USAGE:: + % + % pth = bids.internal.format_path(pth) + % + % Replaces single '\' by '/' in Windows paths + % to prevent escaping warning when printing a path to screen + % + % :param pth: If pth is a cellstr of paths, pathToPrint will work + % recursively on it. + % :type pth: char or cellstr$ + % + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + if isunix() + return + end + + if ischar(pth) + pth = strrep(pth, '\', '/'); + + elseif iscell(pth) + for i = 1:numel(pth) + pth{i} = pathToPrint(pth{i}); + end + end + +end diff --git a/+bids/+internal/get_meta_list.m b/+bids/+internal/get_meta_list.m index a769646e..14aad1da 100644 --- a/+bids/+internal/get_meta_list.m +++ b/+bids/+internal/get_meta_list.m @@ -16,8 +16,10 @@ % metalist - list of paths to metafiles % % + % (C) Copyright 2011-2018 Guillaume Flandin, Wellcome Centre for Human Neuroimaging % + % (C) Copyright 2018 BIDS-MATLAB developers if nargin == 1 @@ -59,6 +61,7 @@ % For all those files we find which one is potentially associated with % the file of interest + % TODO: not more than one file per level is allowed for i = 1:numel(metafile) p2 = bids.internal.parse_filename(metafile{i}); @@ -76,6 +79,8 @@ % as its data file counterpart ismeta = true; if ~strcmp(p.suffix, p2.suffix) + % TODO is this necessary as we have already + % only listed files with the same suffix ismeta = false; end for j = 1:numel(entities) diff --git a/+bids/+internal/get_metadata.m b/+bids/+internal/get_metadata.m index b30334ec..f6997e40 100644 --- a/+bids/+internal/get_metadata.m +++ b/+bids/+internal/get_metadata.m @@ -16,8 +16,10 @@ % add explanation on how the inheritance principle is implemented. % % + % (C) Copyright 2011-2018 Guillaume Flandin, Wellcome Centre for Human Neuroimaging % + % (C) Copyright 2018 BIDS-MATLAB developers meta = struct(); diff --git a/+bids/+internal/get_version.m b/+bids/+internal/get_version.m new file mode 100644 index 00000000..0a9f26b5 --- /dev/null +++ b/+bids/+internal/get_version.m @@ -0,0 +1,26 @@ +function version_number = get_version() + % + % Reads the version number of the pipeline from the txt file in the root of the + % repository. + % + % USAGE:: + % + % version_number = bids.internal.get_version() + % + % :returns: :version_number: (string) Use semantic versioning format (like v0.1.0) + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + try + version_number = fileread(fullfile(fileparts(mfilename('fullpath')), ... + '..', '..', 'version.txt')); + catch + + version_number = 'v0.1.0dev '; + + end + + % dirty hack to get rid of line return + version_number = version_number(1:end - 1); +end diff --git a/+bids/+internal/is_github_ci.m b/+bids/+internal/is_github_ci.m index 8e59ce79..ce3ff300 100644 --- a/+bids/+internal/is_github_ci.m +++ b/+bids/+internal/is_github_ci.m @@ -1,4 +1,5 @@ function [is_github, pth] = is_github_ci() + % (C) Copyright 2021 Remi Gau is_github = false; diff --git a/+bids/+internal/is_octave.m b/+bids/+internal/is_octave.m index f8ad5e4b..e866e031 100644 --- a/+bids/+internal/is_octave.m +++ b/+bids/+internal/is_octave.m @@ -6,8 +6,9 @@ % % status = isOctave() % - % :returns: :status: (boolean) + % :returns: :status: (logical) % + % (C) Copyright 2020 Agah Karakuzu persistent cacheval % speeds up repeated calls diff --git a/+bids/+internal/is_valid_fieldname.m b/+bids/+internal/is_valid_fieldname.m index 4bbc4806..e70499a2 100644 --- a/+bids/+internal/is_valid_fieldname.m +++ b/+bids/+internal/is_valid_fieldname.m @@ -8,6 +8,7 @@ % % status = is_valid_fieldname(some_str) % + % (C) Copyright 2022 BIDS-MATLAB developers status = true; diff --git a/+bids/+internal/keep_file_for_query.m b/+bids/+internal/keep_file_for_query.m index f78c1340..a5c504b2 100644 --- a/+bids/+internal/keep_file_for_query.m +++ b/+bids/+internal/keep_file_for_query.m @@ -6,6 +6,7 @@ % % returns ``false`` if the file is to be kept when running ``bids.query`` % + % (C) Copyright 2021 BIDS-MATLAB developers status = true; @@ -143,6 +144,9 @@ return end if ~strcmp(option(1), '^') + if isnumeric(option) + option = char(option); + end option = ['^' option]; end if ~strcmp(option(end), '$') diff --git a/+bids/+internal/list_all_trial_types.m b/+bids/+internal/list_all_trial_types.m new file mode 100644 index 00000000..32fc3e51 --- /dev/null +++ b/+bids/+internal/list_all_trial_types.m @@ -0,0 +1,104 @@ +function trial_type_list = list_all_trial_types(varargin) + % + % List all the trial_types in all the events.tsv files for a task. + % + % USAGE:: + % + % trial_type_list = bids.internal.list_all_trial_types(BIDS, , ... + % task, ... + % 'trial_type_col', 'trial_type', ... + % 'tolerant', true, ... + % 'verbose', false) + % + % + % :param BIDS: BIDS directory name or BIDS structure (from ``bids.layout``) + % :type BIDS: structure or string + % + % :param task: name of the task + % :type task: char + % + % :param trial_type_col: Optional. Name of the column containing the trial type. + % Defaults to ``'trial_type'``. + % :type trial_type_col: char + % + % :param tolerant: Optional. Default to ``true``. + % :type tolerant: logical + % + % :param verbose: Optional. Default to ``false``. + % :type verbose: logical + % + % + + % (C) Copyright 2022 Remi Gau + + default_tolerant = true; + default_verbose = false; + + is_dir_or_struct = @(x) (isstruct(x) || isdir(x)); + + args = inputParser(); + addRequired(args, 'BIDS', is_dir_or_struct); + addRequired(args, 'task'); + addParameter(args, 'modality', '.*', @ischar); + addParameter(args, 'trial_type_col', 'trial_type', @ischar); + addParameter(args, 'tolerant', default_tolerant); + addParameter(args, 'verbose', default_verbose); + + parse(args, varargin{:}); + + BIDS = args.Results.BIDS; + task = args.Results.task; + modality = args.Results.modality; + trial_type_col = args.Results.trial_type_col; + tolerant = args.Results.tolerant; + verbose = args.Results.verbose; + + trial_type_list = {}; + + event_files = bids.query(BIDS, 'data', ... + 'suffix', 'events', ... + 'extension', '.tsv', ... + 'task', task); + + if isempty(event_files) + msg = sprintf('No events.tsv files for tasks:%s', ... + bids.internal.create_unordered_list(task)); + bids.internal.error_handling(mfilename(), 'noEventsFile', ... + msg, ... + tolerant, ... + verbose); + return + end + + no_trial_type_column = true; + for i = 1:size(event_files, 1) + content = bids.util.tsvread(event_files{i, 1}); + if isfield(content, trial_type_col) + trial_type = content.(trial_type_col); + no_trial_type_column = false; + if ~iscell(trial_type) && all(isnumeric(trial_type)) + trial_type = cellstr(num2str(trial_type)); + end + trial_type_list = cat(1, trial_type_list, trial_type); + trial_type_list = unique(trial_type_list); + end + end + + if no_trial_type_column + msg = sprintf('No "%s" column found in files:%s', ... + trial_type_col, ... + bids.internal.create_unordered_list(bids.internal.format_path(event_files))); + bids.internal.error_handling(mfilename(), 'noTrialTypeColumn', ... + msg, ... + tolerant, ... + verbose); + return + end + + trial_type_list = unique(trial_type_list); + idx = ismember(trial_type_list, trial_type_col); + if any(idx) + trial_type_list{idx} = []; + end + +end diff --git a/+bids/+internal/list_events.m b/+bids/+internal/list_events.m new file mode 100644 index 00000000..b56d9cb4 --- /dev/null +++ b/+bids/+internal/list_events.m @@ -0,0 +1,143 @@ +function [data, headers, y_labels] = list_events(varargin) + % + % Returns summary of all events for a given task. + % + % USAGE:: + % + % [data, headers, y_labels] = bids.internal.list_events(BIDS, ... + % modality, ... + % task, ... + % 'filter', struct(), ... + % 'trial_type_col', 'trial_type') + % + % + % :param BIDS: BIDS directory name or BIDS structure (from ``bids.layout``) + % :type BIDS: structure or string + % + % :param modality: name of the modality + % :type modality: char + % + % :param task: name of the task + % :type task: char + % + % :param filter: Optional. List of filters to choose what files to copy + % (see bids.query). Default to ``struct()``. + % :type filter: structure or cell + % + % :param trial_type_col: Optional. Name of the column containing the trial type. + % Defaults to ``'trial_type'``. + % :type trial_type_col: char + % + % See also: bids.diagnostic, bids.internal.plot_diagnostic_table + % + + % (C) Copyright 2022 Remi Gau + + is_dir_or_struct = @(x) (isstruct(x) || isdir(x)); + + args = inputParser(); + addRequired(args, 'BIDS', is_dir_or_struct); + addRequired(args, 'modality'); + addRequired(args, 'task'); + addParameter(args, 'filter', struct(), @isstruct); + addParameter(args, 'trial_type_col', 'trial_type', @ischar); + + parse(args, varargin{:}); + + BIDS = args.Results.BIDS; + modality = args.Results.modality; + task = args.Results.task; + trial_type_col = args.Results.trial_type_col; + filter = args.Results.filter; + + BIDS = bids.layout(BIDS); + + this_filter = filter; + this_filter.task = task; + this_filter.modality = modality; + subjects = bids.query(BIDS, 'subjects', this_filter); + + y_labels = {}; + headers = {}; + data = []; + + if isempty(subjects) + return + end + + trial_type_list = bids.internal.list_all_trial_types(BIDS, task, ... + 'tolerant', true, 'verbose', true); + + if isempty(trial_type_list) + data = []; + return + end + + % get number of events file to initialize table + this_filter = get_clean_filter(filter, subjects, modality, task); + event_files = bids.query(BIDS, 'data', this_filter); + data = zeros(numel(event_files), numel(trial_type_list)); + + for i = 1:numel(trial_type_list) + headers{i}.modality{1} = strrep(trial_type_list{i}, '_', ' '); + end + + row = 1; + for i_sub = 1:numel(subjects) + + this_filter = get_clean_filter(filter, subjects{i_sub}, modality, task); + + sessions = bids.query(BIDS, 'sessions', this_filter); + if isempty(sessions) + sessions = {''}; + end + + for i_sess = 1:numel(sessions) + + this_filter = get_clean_filter(filter, subjects{i_sub}, modality, task); + this_filter.ses = sessions{i_sess}; + + event_files = bids.query(BIDS, 'data', this_filter); + + for i_file = 1:size(event_files, 1) + + this_label = bids.internal.file_utils(event_files{i_file, 1}, 'basename'); + this_label = strrep(this_label, '_', ' '); + this_label = strrep(this_label, ' events', ''); + y_labels{end + 1, 1} = this_label; %#ok<*AGROW> + + content = bids.util.tsvread(event_files{i_file, 1}); + + if ~isfield(content, trial_type_col) + row = row + 1; + continue + end + + trials = content.(trial_type_col); + if ~iscell(trials) && all(isnumeric(trials)) + trials = cellstr(num2str(trials)); + end + + for i_trial_type = 1:numel(trial_type_list) + tmp = ismember(trials, trial_type_list{i_trial_type}); + data(row, i_trial_type) = sum(tmp); + end + + row = row + 1; + + end + + end + + end + +end + +function this_filter = get_clean_filter(filter, sub, modality, task) + this_filter = filter; + this_filter.sub = sub; + this_filter.task = task; + this_filter.modality = modality; + this_filter.suffix = 'events'; + this_filter.extension = '.tsv'; +end diff --git a/+bids/+internal/match_structure_fields.m b/+bids/+internal/match_structure_fields.m index 0a675fa4..21b9eae1 100644 --- a/+bids/+internal/match_structure_fields.m +++ b/+bids/+internal/match_structure_fields.m @@ -7,6 +7,7 @@ % [struct_one, struct_two] = match_structure_fields(struct_one, struct_two) % % + % (C) Copyright 2021 BIDS-MATLAB developers missing_fields = setxor(fieldnames(struct_one), fieldnames(struct_two)); diff --git a/+bids/+internal/parse_filename.m b/+bids/+internal/parse_filename.m index 3e5bcb32..bc44bd57 100644 --- a/+bids/+internal/parse_filename.m +++ b/+bids/+internal/parse_filename.m @@ -1,10 +1,10 @@ -function p = parse_filename(filename, fields, tolerant) +function p = parse_filename(filename, fields, tolerant, verbose) % % Split a filename into its building constituents % % USAGE:: % - % p = bids.internal.parse_filename(filename, fields) + % p = bids.internal.parse_filename(filename, fields, tolerant, verbose) % % :param filename: filename to parse that follows the pattern % ``sub-label[_entity-label]*_suffix.extension`` @@ -50,8 +50,10 @@ % 'prefix', ''); % % + % (C) Copyright 2011-2018 Guillaume Flandin, Wellcome Centre for Human Neuroimaging % + % (C) Copyright 2018 BIDS-MATLAB developers if nargin < 2 || isempty(fields) @@ -62,6 +64,15 @@ tolerant = true; end + if nargin < 4 || isempty(verbose) + verbose = true; + end + + if isempty(filename) + p = struct([]); + return + end + fields_order = {'filename', 'ext', 'suffix', 'entities', 'prefix'}; filename = bids.internal.file_utils(filename, 'filename'); @@ -81,7 +92,7 @@ % Identify extension [basename, p.ext] = strtok(basename, '.'); - p = parse_entity_label_pairs(p, basename, tolerant); + p = parse_entity_label_pairs(p, basename, tolerant, verbose); % Extra fields can be added to the structure and ordered specifically. if ~isempty(fields) @@ -92,15 +103,15 @@ p = orderfields(p, fields_order); p.entities = orderfields(p.entities, fields); catch - msg = sprintf('Ignoring file %s not matching template.', filename); - bids.internal.error_handling(mfilename, 'noMatchingTemplate', msg, tolerant, true); + msg = sprintf('Ignoring file %s not matching template.', bids.internal.format_path(filename)); + bids.internal.error_handling(mfilename, 'noMatchingTemplate', msg, tolerant, verbose); p = struct([]); end end end -function p = parse_entity_label_pairs(p, basename, tolerant) +function p = parse_entity_label_pairs(p, basename, tolerant, verbose) p.entities = struct(); p.suffix = ''; @@ -159,7 +170,7 @@ catch ME msg = sprintf('Entity-label pair ''%s'' of file %s is not valid: %s.', ... - parts{i}, p.filename, ME.message); + parts{i}, bids.internal.format_path(p.filename), ME.message); if tolerant msg = sprintf('%s\n\tThis file will be ignored.', msg); end @@ -167,7 +178,7 @@ bids.internal.error_handling(mfilename, error_id, ... msg, ... tolerant, ... - true); + verbose); if tolerant p = struct([]); diff --git a/+bids/+internal/plot_diagnostic_table.m b/+bids/+internal/plot_diagnostic_table.m new file mode 100644 index 00000000..71f0551a --- /dev/null +++ b/+bids/+internal/plot_diagnostic_table.m @@ -0,0 +1,167 @@ +function plot_diagnostic_table(diagnostic_table, headers, yticklabel, fig_name, visible) + % + % Plot a diagnostic table to see the number of files per subject or of trials per run. + % + % USAGE:: + % + % plot_diagnostic_table(diagnostic_table, headers, yticklabel, fig_name) + % + % :param diagnostic_table: table to plot + % :type diagnostic_table: n X m array of integers + % + % :param headers: Used to created the column names + % :type headers: n X 1 cell of struct + % + % :param yticklabel: + % :type yticklabel: m X 1 cellstr + % + % :param fig_name: + % :type fig_name: str + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + if nargin < 5 + visible = 'on'; + end + + if isempty(diagnostic_table) + return + end + + if ~all(size(diagnostic_table) == [numel(yticklabel), numel(headers)]) + + bids.internal.error_handling(mfilename(), ... + 'tableLabelsMismatch', ... + sprintf(['table dimensions [%i, %i] does not match', ... + ' number of rows (%i) and columns labels (%i)\n'], ... + size(diagnostic_table, 1), size(diagnostic_table, 2), ... + numel(yticklabel), numel(headers)), ... + false); + + end + + xticklabel = create_x_tick_label(headers); + + nb_rows = size(diagnostic_table, 1); + nb_cols = size(diagnostic_table, 2); + + figure('name', 'diagnostic_table', ... + 'position', [1000 1000 50 + 350 * nb_cols 50 + 100 * nb_rows], ... + 'visible', visible); + + hold on; + + colormap('gray'); + + imagesc(diagnostic_table, [0, max(diagnostic_table(:))]); + + % x axis + set(gca, 'XAxisLocation', 'top', ... + 'xTick', 1:nb_cols, ... + 'xTickLabel', xticklabel, ... + 'TickLength', [0.001 0.001]); + + if any(cellfun('length', xticklabel) > 40) + set(gca, ... + 'xTick', (1:nb_cols) - 0.25, ... + 'XTickLabelRotation', 25); + end + + % y axis + set(gca, 'yTick', 1:nb_rows); + + set(gca, 'yTickLabel', yticklabel); + + box(gca, 'on'); + + % TODO + % fix diagnonal line that appear for some table dimensions + + % add horizontal borders + x_borders = [0 nb_cols] + 0.5; + y_borders = [[2:nb_rows]', [2:nb_rows]'] - 0.5; + if numel(x_borders) == numel(y_borders) + plot(x_borders, y_borders, '-w'); + end + + % add vertical borders + y_borders = [0 nb_rows] + 0.5; + x_borders = [[2:nb_cols]', [2:nb_cols]'] - 0.5; + if numel(x_borders) == numel(y_borders) + plot(x_borders, y_borders, '-w'); + end + + % % tried using grid to use as borders + % % but there seems to always be the main grid overlaid on the values + % + % set(gca, 'XMinorGrid', 'on', 'YMinorGrid', 'off', ... + % 'MinorGridColor', 'w', ... + % 'MinorGridAlpha', 0.5, ... + % 'MinorGridLineStyle', '-', ... + % 'LineWidth', 2, ... + % 'Layer', 'top'); + % + % ca = gca; + % + % % the following lines crash on Octave + % ca.XAxis.MinorTickValues = ca.XAxis.TickValues(1:) + 0.5; + % ca.YAxis.MinorTickValues = ca.YAxis.TickValues + 0.5; + + axis tight; + + % plot actual values if there are not too many + if numel(diagnostic_table) < 600 + for col = 1:nb_cols + for row = 1:nb_rows + t = text(col, row, sprintf('%i', diagnostic_table(row, col))); + set(t, 'Color', 'blue'); + if diagnostic_table(row, col) == 0 + set(t, 'Color', 'red'); + end + end + end + end + + colorbar(); + + title(fig_name); + +end + +function xticklabel = create_x_tick_label(headers) + + for col = 1:numel(headers) + + if iscell(headers{col}.modality) + xticklabel{col} = headers{col}.modality{1}; + else + xticklabel{col} = headers{col}.modality; + end + + xticklabel = append_entity_to_label(headers, xticklabel, col, 'task'); + + xticklabel = append_entity_to_label(headers, xticklabel, col, 'suffix'); + + if length(xticklabel{col}) > 43 + xticklabel{col} = [xticklabel{col}(1:40) '...']; + end + + end + +end + +function label = append_entity_to_label(headers, label, col, entity) + + if isfield(headers{col}, entity) + + if iscell(headers{col}.(entity)) + headers{col}.(entity) = headers{col}.(entity){1}; + end + + label{col} = sprintf(['%s - ' entity ': %s'], label{col}, headers{col}.(entity)); + + end + +end diff --git a/+bids/+internal/regexify.m b/+bids/+internal/regexify.m new file mode 100644 index 00000000..9906a564 --- /dev/null +++ b/+bids/+internal/regexify.m @@ -0,0 +1,29 @@ +function string = regexify(string) + % + % Turns a string into a simple regex. + % Useful to query bids dataset with bids.query + % that by default expects will treat its inputs as regexp. + % + % Input --> Output + % + % ``foo`` --> ``^foo$`` + % + % USAGE:: + % + % string = bids.internal.regexify(string) + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + if isempty(string) + string = '^$'; + return + end + if ~strcmp(string(1), '^') + string = ['^' string]; + end + if ~strcmp(string(end), '$') + string = [string '$']; + end +end diff --git a/+bids/+internal/replace_placeholders.m b/+bids/+internal/replace_placeholders.m index 30306497..ae08dd69 100644 --- a/+bids/+internal/replace_placeholders.m +++ b/+bids/+internal/replace_placeholders.m @@ -1,5 +1,6 @@ function boilerplate_text = replace_placeholders(boilerplate_text, metadata) % + % (C) Copyright 2018 BIDS-MATLAB developers placeholders = return_list_placeholders(boilerplate_text); diff --git a/+bids/+internal/return_file_index.m b/+bids/+internal/return_file_index.m index 3ed64208..6d35b4d3 100644 --- a/+bids/+internal/return_file_index.m +++ b/+bids/+internal/return_file_index.m @@ -7,6 +7,7 @@ % % file_idx = return_file_index(BIDS, modality, filename) % + % (C) Copyright 2021 BIDS-MATLAB developers sub_idx = bids.internal.return_subject_index(BIDS, filename); diff --git a/+bids/+internal/return_file_info.m b/+bids/+internal/return_file_info.m index ed899adc..088c69f7 100644 --- a/+bids/+internal/return_file_info.m +++ b/+bids/+internal/return_file_info.m @@ -4,6 +4,7 @@ % % file_info = return_file_info(BIDS, fullpath_filename) % + % (C) Copyright 2021 BIDS-MATLAB developers file_info.path = bids.internal.file_utils(fullpath_filename, 'path'); diff --git a/+bids/+internal/return_subject_index.m b/+bids/+internal/return_subject_index.m index 0639a7f5..74d8b04f 100644 --- a/+bids/+internal/return_subject_index.m +++ b/+bids/+internal/return_subject_index.m @@ -8,6 +8,7 @@ % sub_idx = return_subject_index(BIDS, filename) % % + % (C) Copyright 2021 BIDS-MATLAB developers parsed_file = bids.internal.parse_filename(filename); diff --git a/+bids/+internal/root_dir.m b/+bids/+internal/root_dir.m index 3f1eecf2..98d2f3ab 100644 --- a/+bids/+internal/root_dir.m +++ b/+bids/+internal/root_dir.m @@ -1,7 +1,12 @@ function pth = root_dir() % + % (C) Copyright 2021 BIDS-MATLAB developers + if bids.internal.is_octave + warning('off', 'Octave:mixed-string-concat'); + end + pth = fullfile(fileparts(mfilename('fullpath')), '..', '..'); pth = bids.internal.file_utils(pth, 'cpath'); diff --git a/+bids/+internal/starts_with.m b/+bids/+internal/starts_with.m index 37a077f0..f9a053c1 100644 --- a/+bids/+internal/starts_with.m +++ b/+bids/+internal/starts_with.m @@ -14,8 +14,10 @@ % % Based on the equivalent function from SPM12. % + % (C) Copyright 2011-2018 Guillaume Flandin, Wellcome Centre for Human Neuroimaging % + % (C) Copyright 2018 BIDS-MATLAB developers res = false; diff --git a/+bids/+internal/url.m b/+bids/+internal/url.m new file mode 100644 index 00000000..4f598302 --- /dev/null +++ b/+bids/+internal/url.m @@ -0,0 +1,46 @@ +function value = url(section) + % + % returns URL of some specific sections of the spec + % + % USAGE:: + % + % value = url(section) + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + supported_sections = {'base', 'agnostic-files', 'participants', 'samples', 'description'}; + + switch section + + case 'base' + value = 'https://bids-specification.readthedocs.io/en/latest/'; + + case 'agnostic-files' + value = [bids.internal.url('base'), '03-modality-agnostic-files.html']; + + case 'participants' + value = [bids.internal.url('agnostic-files'), '#participants-file']; + + case 'samples' + value = [bids.internal.url('agnostic-files'), '#samples-file']; + + case 'sessions' + value = [bids.internal.url('agnostic-files'), '#sessions-file']; + + case 'scans' + value = [bids.internal.url('agnostic-files'), '#scans-file']; + + case 'description' + value = [bids.internal.url('agnostic-files'), '#dataset_descriptionjson']; + + otherwise + bids.internal.error_handling(mfilename(), ... + 'unknownUrlRequest', ... + sprintf('Section %s unsupported. Supported sections: %s', ... + section, ... + strjoin(supported_sections, ', ')), ... + false); + + end +end diff --git a/+bids/+transformers_list/Assign.m b/+bids/+transformers_list/Assign.m new file mode 100644 index 00000000..542d78f2 --- /dev/null +++ b/+bids/+transformers_list/Assign.m @@ -0,0 +1,251 @@ +function data = Assign(transformer, data) + % + % The Assign transformation assigns one or more variables or columns (specified as the input) + % to one or more other columns (specified by target and/or output as described below). + % + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "Assign", + % "Input": ["response_time"], + % "Target": ["face"], + % "TargetAttr": "duration", + % "Output": ["face_modulated_by_RT"] + % } + % + % + % Arguments: + % + % :param Input: **mandatory**. The name(s) of the columns + % from which attribute values are to be drawn + % (for assignment to the attributes of other columns). + % Must exactly match the length of the target argument. + % :type Input: string or array + % + % :param Target: **mandatory**. the name(s) of the columns to which + % the attribute values taken from the input + % are to be assigned. + % Must exactly match the length of the input argument. + % Names are mapped 1-to-1 from input to target. + % :type Target: string or array + % + % .. note:: + % + % If no output argument is specified, the columns named in target are modified in-place. + % + % + % :param Output: Optional. Names of the columns to output the result of the assignment to. + % Must exactly match the length of the input and target arguments. + % :type Output: string or array + % + % If no output array is provided, columns named in target are modified in-place. + % + % If an output array is provided: + % + % - each column in the target array is first cloned, + % - then the reassignment from the input to the target is applied; + % - finally, the new (cloned and modified) column is written out to the column named in output. + % + % :param InputAttr: Optional. Specifies which attribute of the input column to assign. + % Defaults to ``value``. + % If a array is passed, its length must exactly match + % that of the input and target arrays. + % :type InputAttr: string or array + % + % :param TargetAttr: Optional. Specifies which attribute of the output column to assign to. + % Defaults to ``value``. + % If a array is passed, its length must exactly match + % that of the input and target arrays. + % :type TargetAttr: string or array + % + % ``InputAttr`` and ``TargetAttr`` must be one of: + % + % - ``value``, + % - ``onset``, + % - or ``duration``. + % + % .. note:: + % + % This transformation is non-destructive with respect to the input column(s). + % In case where in-place assignment is desired (essentially, renaming a column), + % either use the rename transformation, or set output to the same value as the input. + % + % Examples: + % + % To reassign the value property of a variable named ``response_time`` + % to the duration property of a ``face`` variable + % (as one might do in order to, e.g., model trial-by-trial reaction time differences + % for a given condition using a varying-epoch approach), + % and write it out as a new ``face_modulated_by_RT`` column. + % + % + % **CODE EXAMPLE**:: + % + % transformer = struct('Name', 'Assign', ... + % 'Input', 'response_time', ... + % 'Target', 'face', ... + % 'TargetAttr', 'duration', ... + % 'Ouput', 'face_modulated_by_RT'); + % + % data.response_time = ; + % data.face = ; + % data.duration = ; + % + % data = bids.transformers(transformer, data); + % + % data.face_modulated_by_RT + % + % ans = + % + % + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + % TODO check if attr are cells + + input = bids.transformers_list.get_input(transformer, data); + target = get_target(transformer, data); + + output = bids.transformers_list.get_output(transformer, data, false); + + input_attr = get_attribute(transformer, input, 'InputAttr'); + target_attr = get_attribute(transformer, input, 'TargetAttr'); + + for i = 1:numel(input) + + if ~isfield(data, input{i}) + continue + end + + if ~isempty(output) + assign_to = output{i}; + else + assign_to = target{i}; + end + + % grab the data that is being assigned somewhere else + % TODO deal with cell + % TODO deal with grabbing only certain rows? + switch input_attr{i} + + case 'value' + to_assign = data.(input{i}); + + case {'onset', 'duration'} + + attr_to_assign = data.(input_attr{i}); + + if strcmp(target_attr, 'value') + to_assign = attr_to_assign; + else + to_assign = data.(input{i}); + end + + otherwise + bids.internal.error_handling(mfilename(), 'wrongAttribute', ... + 'InputAttr must be one of "value", "onset", "duration"', ... + false); + + end + + if ~ismember(target_attr{i}, {'value', 'onset', 'duration'}) + bids.internal.error_handling(mfilename(), 'wrongAttribute', ... + 'InputAttr must be one of "value", "onset", "duration"', ... + false); + end + + if strcmp(target_attr, 'value') + + data.(assign_to) = to_assign; + + else + + fields = fieldnames(data); + for j = 1:numel(fields) + + if ismember(fields{j}, {assign_to, input{i}}) + continue + + elseif ismember(fields{j}, {target_attr{i}}) + data.(target_attr{i}) = cat(1, data.(target_attr{i}), to_assign); + + elseif ismember(fields{j}, {'onset', 'duration'}) + data.(fields{j}) = repmat(data.(fields{j}), 2, 1); + + else + + % pad non concerned fields with nan + data = pad_with_nans(data, fields{j}, to_assign); + end + + end + + % pad concerned fields + data.(assign_to) = cat(1, nan(size(to_assign)), data.(assign_to)); + data = pad_with_nans(data, input{i}, to_assign); + + end + + end + +end + +function data = pad_with_nans(data, field, to_assign) + + if iscell(data.(field)) + data.(field) = cat(1, data.(field), repmat({nan}, numel(to_assign), 1)); + else + data.(field) = cat(1, data.(field), nan(size(to_assign))); + end + +end + +function attr = get_attribute(transformer, input, type) + + attr = {'value'}; + if isfield(transformer, type) + attr = transformer.(type); + if ischar(attr) + attr = {attr}; + end + if numel(attr) > 1 && numel(attr) ~= numel(input) + bids.internal.error_handling(mfilename(), 'missingAttribute', ... + sprintf(['If number of %s must be equal to 1 ', ... + 'or to the number of Inputs'], type), ... + false); + end + end + +end + +function target = get_target(transformer, data) + + if isfield(transformer, 'Target') + + target = transformer.Target; + + if isempty(target) + target = {}; + if isfield(transformer, 'verbose') + warning('empty "Target" field'); + end + return + + else + bids.transformers_list.check_field(transformer.Target, data, 'Target', false); + + end + + target = {target}; + + else + target = {}; + + end + +end diff --git a/+bids/+transformers_list/BaseTransformer.m b/+bids/+transformers_list/BaseTransformer.m new file mode 100644 index 00000000..9bf1d88d --- /dev/null +++ b/+bids/+transformers_list/BaseTransformer.m @@ -0,0 +1,202 @@ +classdef BaseTransformer + % + % WIP in case we need to object oriented + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + properties + + input % cell + output % cell + data % structure + verbose = false % logical + overwrite = true % logical + + end + + methods + + function obj = BaseTransformer(varargin) + + args = inputParser; + + args.addOptional('transformer', struct([]), @isstruct); + args.addOptional('data', struct([]), @isstruct); + args.addOptional('overwrite', obj.overwrite, @islogical); + args.addOptional('verbose', obj.verbose, @islogical); + + args.parse(varargin{:}); + + obj.overwrite = args.Results.overwrite; + obj.verbose = args.Results.verbose; + + if ~isempty(args.Results.transformer) + obj.input = obj.get_input(args.Results.transformer); + obj.output = obj.get_output(args.Results.transformer); + end + + if ~isempty(args.Results.data) + obj.data = args.Results.data; + obj.check_input(obj.input, obj.data); + end + + end + + %% Getters + function value = get.input(obj) + value = obj.input; + end + + function value = get.output(obj) + value = obj.output; + end + + function data = get.data(obj) + data = obj.data; + end + + %% Setters + function obj = set.input(obj, input) + obj.input = input; + end + + function obj = set.data(obj, data) + obj.data = data; + end + + function obj = set.output(obj, output) + obj.output = output; + end + + %% complex getters + function input = get_input(obj, transformer) + + if nargin < 2 + input = obj.input; + return + end + + assert(isstruct(transformer)); + assert(numel(transformer) == 1); + + if isfield(transformer, 'Input') + + input = transformer.Input; + + input = validate_input(obj, input); + + else + input = {}; + return + + end + + end + + function output = get_output(obj, transformer) + + if nargin < 2 + output = obj.output; + return + end + + assert(isstruct(transformer)); + assert(numel(transformer) == 1); + + if isfield(transformer, 'Output') + + output = transformer.Output; + + output = validate_output(obj, output); + + else + if obj.overwrite + output = obj.input; + else + output = {}; + end + + end + + end + + function data = get_data(obj, field, rows) + if nargin < 3 + data = obj.data(field); + else + data = obj.data(field); + data = data(rows); + end + end + + %% + function check_field(obj, field_list, data, field_type) + % + % check that each field in field_list is present + % in the data structure + % + + available_variables = fieldnames(data); + + available_from_fieldlist = ismember(field_list, available_variables); + + if ~all(available_from_fieldlist) + msg = sprintf('missing variable(s): "%s"', ... + strjoin(field_list(~available_from_fieldlist), '", "')); + bids.internal.error_handling(mfilename(), ['missing' field_type], msg, false); + end + + end + + function check_input(obj, input, data) + obj.check_field(input, data, 'Input'); + end + + end + + methods (Access = private) + + function input = validate_input(obj, input) + + if isempty(input) + input = {}; + if obj.verbose + warning('empty "Input" field'); + end + return + end + + if ~iscell(input) + input = {input}; + end + + end + + function output = validate_output(obj, output) + + if isempty(output) + output = {}; + if obj.verbose + warning('empty "Output" field'); + end + return + + else + if obj.overwrite + output = obj.input; + else + output = {}; + end + + end + + if ~iscell(output) + output = {output}; + end + + end + + end + +end diff --git a/+bids/+transformers_list/Basic.m b/+bids/+transformers_list/Basic.m new file mode 100644 index 00000000..ac23571f --- /dev/null +++ b/+bids/+transformers_list/Basic.m @@ -0,0 +1,135 @@ +function data = Basic(transformer, data) + % + % Performs a basic operation with a ``Value`` on the ``Input`` + % + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "Add", + % "Input": "onset", + % "Value": 0.5, + % "Output": "delayed_onset" + % "Query": "familiarity == Famous face" + % } + % + % Each of these transformations takes one or more columns, + % and performs a mathematical operation on the input column and a provided operand. + % The operations are performed on each column independently. + % + % + % Arguments: + % + % :param Name: **mandatory**. Any of ``Add``, ``Subtract``, ``Multiply``, ``Divide``, ``Power``. + % :type Input: string + % + % :param Input: **mandatory**. A array of columns to perform operation on. + % :type Input: string or array + % + % :param Value: **mandatory**. The value to perform operation with (i.e. operand). + % :type Value: float + % + % :param Query: Optional. logical expression used to select on which rows to + % act. + % :type Query: string + % + % :param Output: Optional. List of column names to write out to. + % :type Output: string or array + % + % By default, computation is done in-place on the input + % (meaning that input columns are overwritten). + % If provided, the number of values must exactly match the number of input values, + % and the order will be mapped 1-to-1. + % + % + % **CODE EXAMPLE**:: + % + % transformer = struct('Name', 'Subtract', ... + % 'Input', 'onset', ... + % 'Value', 3, ... + % 'Ouput', 'onset_minus_3'); + % + % data.onset = [1; 2; 5; 6]; + % + % data = bids.transformers(transformer, data); + % + % data.onset_minus_3 + % + % ans = + % + % -2 + % -1 + % 2 + % 3 + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + input = bids.transformers_list.get_input(transformer, data); + output = bids.transformers_list.get_output(transformer, data); + + rows = true(size(data.(input{1}))); + + [left, query_type, right] = bids.transformers_list.get_query(transformer); + if ~isempty(query_type) + + bids.transformers_list.check_field(left, data, 'query', false); + + rows = bids.transformers_list.identify_rows(data, left, query_type, right); + + end + + for i = 1:numel(input) + + if ~isfield(data, input{i}) + continue + end + + value = transformer.Value; + + if ischar(value) + value = str2double(value); + if isnan(value) + msg = sprintf('basic transformers require values convertible to numeric. Got: %s', ... + transformer.Value); + bids.internal.error_handling(mfilename(), ... + 'numericOrCoercableToNumericRequired', ... + msg, ... + false); + end + end + + assert(isnumeric(value)); + + tmp = data.(input{i}); + if iscellstr(tmp) %#ok + tmp = str2num(char(tmp)); %#ok + end + + switch lower(transformer.Name) + + case 'add' + tmp = tmp + value; + + case 'subtract' + tmp = tmp - value; + + case 'multiply' + tmp = tmp * value; + + case 'divide' + tmp = tmp / value; + + case 'power' + tmp = tmp.^value; + + end + + data.(output{i})(rows, :) = tmp(rows, :); + + end + +end diff --git a/+bids/+transformers_list/Concatenate.m b/+bids/+transformers_list/Concatenate.m new file mode 100644 index 00000000..030032d2 --- /dev/null +++ b/+bids/+transformers_list/Concatenate.m @@ -0,0 +1,97 @@ +function data = Concatenate(transformer, data) + % + % Concatenate columns together. + % + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "Concatenate", + % "Input": [ + % "face_type", + % "face_repetition" + % ], + % "Output": "face_type_repetition" + % } + % + % + % Arguments: + % + % :param Input: **mandatory**. Column(s) to concatenate. Must all be of the same length. + % :type Input: array + % + % :param Output: Optional. Name of the output column. + % :type Output: string + % + % **CODE EXAMPLE**:: + % + % transformer = struct('Name', 'Concatenate', ... + % 'Input', {{'face_type', 'face_repetition'}}, ... + % 'Ouput', 'face_type_repetition'); + % + % data.face_type = {'familiar'; 'unknwown'; 'new'; 'familiar'; 'unknwown'; 'new'}; + % data.face_repetition = [1;1;1;2;2;2]; + % + % data = bids.transformers(transformer, data); + % + % data.face_type_repetition + % + % ans = + % { + % 'familiar_1' + % 'unknwown_1' + % 'new_1' + % 'familiar_2' + % 'unknwown_2' + % 'new_2' + % } + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + input = bids.transformers_list.get_input(transformer, data); + if any(~ismember(input, fieldnames(data))) + return + end + output = bids.transformers_list.get_output(transformer, data, false); + + nb_rows = []; + for i = 1:numel(input) + nb_rows(i) = size(data.(input{i}), 1); %#ok + end + nb_rows = unique(nb_rows); + assert(length(nb_rows) == 1); + + for row = 1:nb_rows + + tmp1 = {}; + + for i = 1:numel(input) + + if isnumeric(data.(input{i})) + tmp1{1, i} = num2str(data.(input{i})(row)); + + elseif iscellstr(data.(input{i})) + tmp1{1, i} = data.(input{i}){row}; + + elseif iscell(data.(input{i})) + tmp1{1, i} = data.(input{i}){row}; + + if isnumeric(tmp1{1, i}) + tmp1{1, i} = num2str(tmp1{1, i}); + end + + end + + end + + tmp2{row, 1} = strjoin(tmp1, '_'); + + end + + data.(output{1}) = tmp2; + +end diff --git a/+bids/+transformers_list/Constant.m b/+bids/+transformers_list/Constant.m new file mode 100644 index 00000000..55d59b68 --- /dev/null +++ b/+bids/+transformers_list/Constant.m @@ -0,0 +1,59 @@ +function data = Constant(transformer, data) + % + % Adds a new column with a constant value (numeric or char). + % + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "Constant", + % "Value": 1, + % "Output": "intercept" + % } + % + % + % Arguments: + % + % :param Output: **mandatory**. Name of the newly generated column. + % :type Output: string or array + % + % :param Value: Optional. The value of the constant, defaults to ``1``. + % :type Value: float or char + % + % + % **CODE EXAMPLE**:: + % + % transformer = struct('Name', 'Constant', ... + % 'Value', 1, ... + % 'Output', 'intercept'); + % + % + % data = bids.transformers(transformer, data); + % + % + % ans = TODO + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + output = bids.transformers_list.get_output(transformer, data); + + assert(numel(output) == 1); + + value = 1; + if isfield(transformer, 'Value') + value = transformer.Value; + end + + a = structfun(@(x) size(x, 1), data, 'UniformOutput', false); + nb_rows = max(cell2mat(struct2cell(a))); + if isnumeric(value) + data.(output{1}) = ones(nb_rows, 1) * value; + elseif ischar(value) + data.(output{1}) = cellstr(repmat(value, nb_rows, 1)); + end + +end diff --git a/+bids/+transformers_list/Copy.m b/+bids/+transformers_list/Copy.m new file mode 100644 index 00000000..3ed552f5 --- /dev/null +++ b/+bids/+transformers_list/Copy.m @@ -0,0 +1,68 @@ +function data = Copy(transformer, data) + % + % Clones/copies each of the input columns to a new column with identical values + % and a different name. Useful as a basis for subsequent transformations that need + % to modify their input in-place. + % + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "Copy", + % "Input": [ + % "sex_m", + % "age_gt_twenty" + % ], + % "Output": [ + % "tmp_sex_m", + % "tmp_age_gt_twenty" + % ] + % } + % + % + % Arguments: + % + % :param Input: **mandatory**. Column names to copy. + % :type Input: string or array + % + % :param Output: Optional. Names to copy the input columns to. + % Must be same length as input, and columns are mapped one-to-one + % from the input array to the output array. + % :type Output: string or array + % + % + % **CODE EXAMPLE**:: + % + % transformer = struct('Name', 'Copy', ... + % 'Input', 'onset', ... + % 'Ouput', 'onset_copy'); + % + % data.onset = [1,2,3]; + % + % data = bids.transformers(transformer, data); + % + % data.onset_copy + % + % ans = [1,2,3] + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + input = bids.transformers_list.get_input(transformer, data); + output = bids.transformers_list.get_output(transformer, data); + + assert(numel(input) == numel(output)); + + for i = 1:numel(input) + + if ~isfield(data, input{i}) + continue + end + + data.(output{i}) = data.(input{i}); + end + +end diff --git a/+bids/+transformers_list/Delete.m b/+bids/+transformers_list/Delete.m new file mode 100644 index 00000000..d88609ef --- /dev/null +++ b/+bids/+transformers_list/Delete.m @@ -0,0 +1,62 @@ +function data = Delete(transformer, data) + % + % Deletes column(s) from further analysis. + % + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "Delete", + % "Input": [ + % "sex_m", + % "age_gt_twenty" + % ] + % } + % + % + % Arguments: + % + % :param Input: **mandatory**. The name(s) of the columns(s) to delete. + % :type Input: string or array + % + % + % .. note:: + % + % The ``Select`` transformation provides the inverse function + % (selection of columns to keep for subsequent analysis). + % + % + % **CODE EXAMPLE**:: + % + % transformer = struct('Name', 'Delete', ... + % 'Input', {{'sex_m', age_gt_twenty}}); + % + % data.sex_m = TODO; + % data.age_gt_twenty = TODO; + % + % data = bids.transformers(transformer, data); + % + % data. + % + % ans = TODO + % + % + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + input = bids.transformers_list.get_input(transformer, data); + + for i = 1:numel(input) + + if ~isfield(data, input{i}) + continue + end + + data = rmfield(data, input{i}); + end + +end diff --git a/+bids/+transformers_list/Drop_na.m b/+bids/+transformers_list/Drop_na.m new file mode 100644 index 00000000..9cad9d6f --- /dev/null +++ b/+bids/+transformers_list/Drop_na.m @@ -0,0 +1,75 @@ +function data = Drop_na(transformer, data) + % + % Drops all rows with "n/a". + % + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "DropNA", + % "Input": [ + % "age_gt_twenty" + % ], + % "Output": [ + % "age_gt_twenty_clean" + % ] + % } + % + % + % Arguments: + % + % :param Input: **mandatory**. The name of the variable to operate on. + % :type Input: string or array + % + % :param Output: Optional. The column names to write out to. + % By default, computation is done in-place + % meaning that input columnise overwritten). + % :type Output: string or array + % + % + % **CODE EXAMPLE**:: + % + % transformer = struct('Name', 'DropNA', ... + % 'Input', 'age_gt_twenty', ... + % 'Ouput', 'age_gt_twenty_clean'); + % + % data.age_gt_twenty = TODO; + % + % data = bids.transformers(transformer, data); + % + % data. + % + % ans = TODO + % + % + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + input = bids.transformers_list.get_input(transformer, data); + output = bids.transformers_list.get_output(transformer, data); + + for i = 1:numel(input) + + if ~isfield(data, input{i}) + continue + end + + this_input = data.(input{i}); + + if isnumeric(this_input) + nan_values = isnan(this_input); + elseif iscell(this_input) + nan_values = cellfun(@(x) all(isnan(x)), this_input); + end + + this_input(nan_values) = []; + + data.(output{i}) = this_input; + + end + +end diff --git a/+bids/+transformers_list/Factor.m b/+bids/+transformers_list/Factor.m new file mode 100644 index 00000000..b8b33a3b --- /dev/null +++ b/+bids/+transformers_list/Factor.m @@ -0,0 +1,94 @@ +function data = Factor(transformer, data) + % + % Converts a nominal/categorical variable with N unique levels + % to either N indicators (i.e., dummy-coding). + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "Factor", + % "Input": [ + % "gender", + % "age" + % ] + % } + % + % Arguments: + % + % :param Input: **mandatory**. The name(s) of the variable(s) to dummy-code. + % :type Input: string or array + % + % By default it is the first factor level when sorting in alphabetical order + % (e.g., if a condition has levels 'dog', 'apple', and 'helsinki', + % the default reference level will be 'apple'). + % + % The name of the output columns for 2 input columns ``gender`` and ``age`` + % with 2 levels (``M``, ``F``) and (``20``, ``30``) respectivaly + % will of the shape: + % + % - ``gender_F_age_20`` + % - ``gender_F_age_20`` + % - ``gender_M_age_30`` + % - ``gender_M_age_30`` + % + % **CODE EXAMPLE**:: + % + % transformer = struct('Name', 'Factor', ... + % 'Input', {{'gender', 'age'}}); + % + % data.gender = TODO; + % data.age = TODO; + % + % data = bids.transformers(transformer, data); + % + % data.gender_F_age_20 + % + % ans = TODO + % + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + input = bids.transformers_list.get_input(transformer, data); + + for i = 1:numel(input) + + if ~isfield(data, input{i}) + continue + end + + this_input = data.(input{i}); + + % coerce to cellstr + % and get name to append for each level + if iscellstr(this_input) + level_names = []; + + elseif isnumeric(this_input) + this_input = cellstr(num2str(this_input)); + level_names = unique(this_input); + + elseif ischar(this_input) + this_input = cellstr(this_input); + level_names = []; + + end + + levels = unique(this_input); + if isempty(level_names) + level_names = cellstr(num2str([1:numel(levels)]')); + end + + % generate new variables + for j = 1:numel(levels) + field = [input{i} '_' level_names{j}]; + field = regexprep(field, '[^a-zA-Z0-9_]', ''); + data.(field) = ismember(this_input, levels{j}); + end + + end + +end diff --git a/+bids/+transformers_list/Filter.m b/+bids/+transformers_list/Filter.m new file mode 100644 index 00000000..7fa8f80a --- /dev/null +++ b/+bids/+transformers_list/Filter.m @@ -0,0 +1,98 @@ +function data = Filter(transformer, data) + % + % Subsets rows using a logical expression. + % + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "Filter", + % "Input": "sex", + % "Query": "age > 20" + % } + % + % + % Arguments: + % + % :param Input: **mandatory**. The name(s) of the variable(s) to operate on. + % :type Input: string or array + % + % :param Query: **mandatory**. logical expression used to filter + % :type Query: string + % + % Supports: + % + % - ``>``, ``<``, ``>=``, ``<=``, ``==``, ``~=`` for numeric values + % + % - ``==``, ``~=`` for string operation (case sensitive). + % Regular expressions are supported + % + % :param Output: Optional. The optional column names to write out to. + % :type Output: string or array + % + % By default, computation is done in-place (i.e., input columnise overwritten). + % If provided, the number of values must exactly match the number of input values, + % and the order will be mapped 1-to-1. + % + % + % **CODE EXAMPLE**:: + % + % transformer = struct('Name', 'Filter', ... + % 'Input', 'sex', ... + % 'Query', 'age > 20'); + % + % data.sex = {'M', 'F', 'F', 'M'}; + % data.age = [10, 21, 15, 26]; + % + % data = bids.transformers(transformer, data); + % + % data.sex + % + % ans = + % + % 4X1 cell array + % + % [NaN] + % 'F' + % [NaN] + % 'M' + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + % TODO + % - By(str; optional): Name of column to group filter operation by + + input = bids.transformers_list.get_input(transformer, data); + output = bids.transformers_list.get_output(transformer, data); + + [left, query_type, right] = bids.transformers_list.get_query(transformer); + bids.transformers_list.check_field(left, data, 'query', false); + + rows = bids.transformers_list.identify_rows(data, left, query_type, right); + + % filter rows of all inputs + for i = 1:numel(input) + + if ~isfield(data, input{i}) + continue + end + + clear tmp; + + tmp(rows, 1) = data.(input{i})(rows); + + if iscell(tmp) + tmp(~rows, 1) = repmat({nan}, sum(~rows), 1); + else + tmp(~rows, 1) = nan; + end + + data.(output{i}) = tmp; + + end + +end diff --git a/+bids/+transformers_list/Label_identical_rows.m b/+bids/+transformers_list/Label_identical_rows.m new file mode 100644 index 00000000..8642703d --- /dev/null +++ b/+bids/+transformers_list/Label_identical_rows.m @@ -0,0 +1,193 @@ +function data = Label_identical_rows(transformer, data) + % + % Creates an extra column to index consecutive identical rows in a column. + % The index restarts at 1 with every change of row content. + % This can for example be used to label consecutive events of the same trial_type in + % a block. + % + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "LabelIdenticalRows", + % "Input": "trial_type", + % "Cumulative": False + % } + % + % + % Arguments: + % + % :param Input: **mandatory**. The name(s) of the variable(s) to operate on. + % :type Input: string or array + % + % :param Cumulative: **optional**. Defaults to ``False``. + % If ``True``, the labels are not reset to 0 + % when encoutering new row content. + % :type Cumulative: logical + % + % .. note:: + % + % The labels will be by default be put in a column called Input(i)_label + % + % **CODE EXAMPLE**:: + % + % + % transformers(1).Name = 'LabelIdenticalRows'; + % transformers(1).Input = {'trial_type', 'stim_type'}; + % + % data.trial_type = {'face';'face';'house';'house';'house';'house';'chair'}; + % data.stim_type = {1' ; 1 ;1 ;2 ;5 ;2 ; nan}; + % + % new_content = bids.transformers(transformers, data); + % + % assertEqual(new_content.trial_type_label, [1;2;1;2;3;4;1]); + % assertEqual(new_content.stim_type_label, [1;2;3;1;1;1;1]); + % + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + % TODO: label only if cell content matches some condition + + input = bids.transformers_list.get_input(transformer, data); + output = bids.transformers_list.get_output(transformer, data); + + cumulative = false; + if isfield(transformer, 'Cumulative') + cumulative = transformer.Cumulative; + end + + for i = 1:numel(input) + + if ~isfield(data, input{i}) + continue + end + + if strcmp(output{i}, input{i}) + output{i} = [output{i} '_label']; + end + + if isfield(data, output{i}) + bids.internal.error_handling(mfilename(), ... + 'outputFieldAlreadyExist', ... + sprintf('The output field already "%s" exists', output{i}), ... + false); + end + + this_input = data.(input{i}); + if ischar(this_input) + this_input = {this_input}; + end + + % Use a cell to keep track of the occurrences of each value of this_input + label_counter = init_label_counter(this_input, cumulative); + + previous_value = []; + + for j = 1:numel(this_input) + + this_value = this_input(j); + if iscell(this_value) + this_value = this_value{1}; + end + + is_same = compare_rows(this_value, previous_value); + + if cumulative || (~cumulative && is_same) + label_counter = increment_label_counter(label_counter, this_value); + + elseif ~is_same && ~cumulative + label_counter = reset_label_counter(label_counter, cumulative); + end + + idx = get_index(this_value, label_counter); + data.(output{i})(j, 1) = label_counter{idx, 2}; + + previous_value = this_value; + + end + + end + +end + +function label_counter = init_label_counter(this_input, cumulative) + + if isnumeric(this_input) + + label_counter = unique(this_input); + + % Only keep one nan + nan_values = find(isnan(label_counter)); + label_counter(nan_values(2:end)) = []; + + label_counter = num2cell(label_counter); + + elseif iscellstr(this_input) + label_counter = unique(this_input); + + else + + % get unique char first then numeric + idx = cellfun(@(x) ischar(x), this_input); + tmp = this_input(idx); + label_counter_char = init_label_counter(tmp, cumulative); + + idx = cellfun(@(x) isnumeric(x), this_input); + tmp = this_input(idx); + tmp = cell2mat(tmp); + label_counter_num = init_label_counter(tmp, cumulative); + + label_counter = cat(1, label_counter_char, label_counter_num); + + end + + if isempty(label_counter) + label_counter = {'', 0}; + end + + label_counter = reset_label_counter(label_counter, cumulative); + +end + +function argout = get_index(this_value, label_counter) + if isnan(this_value) + argout = cellfun(@(x) isnumeric(x) && isnan(x), label_counter(:, 1)); + elseif isnumeric(this_value) + argout = cellfun(@(x) isnumeric(x) && x == this_value, label_counter(:, 1)); + elseif ischar(this_value) + argout = cellfun(@(x) ischar(x) && strcmp(x, this_value), label_counter(:, 1)); + end + argout = find(argout); +end + +function label_counter = increment_label_counter(label_counter, this_value) + idx = get_index(this_value, label_counter); + label_counter{idx, 2} = label_counter{idx, 2} + 1; +end + +function label_counter = reset_label_counter(label_counter, cumulative) + default_value = 1; + if cumulative + default_value = 0; + end + for i = 1:size(label_counter, 1) + label_counter{i, 2} = default_value; + end +end + +function is_same = compare_rows(this_value, previous_value) + + is_same = false; + + if isempty(previous_value) + elseif all(isnumeric([this_value, previous_value])) && this_value == previous_value + is_same = true; + elseif all(ischar([this_value, previous_value])) && strcmp(this_value, previous_value) + is_same = true; + end + +end diff --git a/+bids/+transformers_list/Logical.m b/+bids/+transformers_list/Logical.m new file mode 100644 index 00000000..f0d5cdf7 --- /dev/null +++ b/+bids/+transformers_list/Logical.m @@ -0,0 +1,108 @@ +function data = Logical(transformer, data) + % + % Each of these transformations: + % + % - takes 2 or more columns as input + % - performs the corresponding logical operation + % + % - inclusive or + % - conjunction + % - logical negation + % + % - returning a single column as output. + % + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "And", + % "Input": ["sex_m", "age_gt_twenty"], + % "Output": "men_older_twenty" + % } + % + % If non-logical input are passed, it is expected that: + % + % - all zero or nan (for numeric data types), + % - "NaN" or empty (for strings) values + % + % will evaluate to false and all other values will evaluate to true. + % + % + % Arguments: + % + % :param Name: **mandatory**. Any of ``And``, ``Or``, ``Not``. + % :type Input: string + % + % :param Input: **mandatory**. An array of columns to perform operation on. Only 1 for ``Not`` + % :type Input: array + % + % :param Output: Optional. The name of the output column. + % :type Output: string or array + % + % + % **CODE EXAMPLE**:: + % + % transformers = struct('Name', 'And', ... + % 'Input', {{'sex_m', 'age_gt_twenty'}}, ... + % 'Output', 'men_gt_twenty'); + % + % data.age_gt_twenty = [nan; 25; 51; 12]; + % data.sex_m = {'M'; 'M'; nan; 'M'}; + % + % data = bids.transformers(transformer, data); + % + % ans = + % + % 4x1 logical array + % + % 0 + % 1 + % 0 + % 1 + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + % TODO + % for Add Or, if not output just merge the name of the input variables + % TODO "not" can only have one input + + input = bids.transformers_list.get_input(transformer, data); + + output = bids.transformers_list.get_output(transformer, data); + assert(numel(output) == 1); + + % try coerce all input to logical + for i = 1:numel(input) + + if ~isfield(data, input{i}) + continue + end + + if iscell(data.(input{i})) + tmp1 = ~cellfun('isempty', data.(input{i})); + tmp2 = ~cellfun(@(x) all(isnan(x)), data.(input{i})); + tmp(:, i) = all([tmp1 tmp2], 2); + + else + tmp2 = data.(input{i}); + tmp2(isnan(tmp2)) = 0; + tmp(:, i) = logical(tmp2); + + end + + end + + switch lower(transformer.Name) + case 'and' + data.(output{1}) = all(tmp, 2); + case 'or' + data.(output{1}) = any(tmp, 2); + case 'not' + + data.(output{1}) = ~tmp; + end + +end diff --git a/+bids/+transformers_list/Mean.m b/+bids/+transformers_list/Mean.m new file mode 100644 index 00000000..4be2fcf7 --- /dev/null +++ b/+bids/+transformers_list/Mean.m @@ -0,0 +1,87 @@ +function data = Mean(transformer, data) + % + % Compute mean of a column. + % + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "Mean", + % "Input": "reaction_time", + % "OmitNan": false, + % "Output": "mean_RT" + % } + % + % + % Arguments: + % + % :param Input: **mandatory**. The name of the variable to operate on. + % :type Input: string or array + % + % :param OmitNan: Optional. If ``false`` any column with nan values will return a nan value. + % If ``true`` nan values are skipped. Defaults to ``false``. + % :type OmitNan: logical + % + % :param Output: Optional. The optional column names to write out to. + % By default, computation is done in-place (i.e., input columnise overwritten). + % :type Output: string or array + % + % + % **CODE EXAMPLE**:: + % + % transformer = struct('Name', 'Mean', ... + % 'Input', 'reaction_time', ... + % 'OmitNan', false, ... + % 'Ouput', 'mean_RT'); + % + % data.reaction_time = TODO + % + % data = bids.transformers(transformer, data); + % + % data.mean_RT = TODO + % + % ans = TODO + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + overwrite = false; + + input = bids.transformers_list.get_input(transformer, data); + output = bids.transformers_list.get_output(transformer, data, overwrite); + + if ~isempty(output) + assert(numel(input) == numel(output)); + end + + if isfield(transformer, 'OmitNan') + omit_nan = transformer.OmitNan; + else + omit_nan = false; + end + + for i = 1:numel(input) + + if ~isfield(data, input{i}) + continue + end + + output_column = [input{i} '_mean']; + if ~isempty(output) + output_column = output{i}; + end + + if omit_nan + data.(output_column) = mean(data.(input{i}), 'omitnan'); + + else + data.(output_column) = mean(data.(input{i})); + + end + + end + +end diff --git a/+bids/+transformers_list/Merge_identical_rows.m b/+bids/+transformers_list/Merge_identical_rows.m new file mode 100644 index 00000000..bab6c03e --- /dev/null +++ b/+bids/+transformers_list/Merge_identical_rows.m @@ -0,0 +1,165 @@ +function new_data = Merge_identical_rows(transformer, data) + % + % Merge consecutive identical rows. + % + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "MergeIdenticalRows", + % "Input": "trial_type", + % } + % + % + % Arguments: + % + % :param Input: **mandatory**. The name(s) of the variable(s) to operate on. + % :type Input: string or array + % + % .. note:: + % + % - Only works on data commit from event.tsv + % - Content is sorted by onset time before merging + % - If multiple variables are specified, they are merged in the order they are specified + % - If a variable is not found, it is ignored + % - If a variable is found, but is empty, it is ignored + % - The content of the other columns corresponds to the last row being merged: + % this means that the content from other columns but the one specified in will be deleted + % execpt for the last one + % + % **CODE EXAMPLE**:: + % + % transformers(1).Name = 'MergeIdenticalRows'; + % transformers(1).Input = {'trial_type'}; + % + % data.trial_type = {'house' ; 'face' ; 'face'; 'house'; 'chair'; 'house' ; 'chair'}; + % data.duration = [1 ; 1 ; 1 ; 1 ; 1 ; 1 ; 1]; + % data.onset = [3 ; 1 ; 2 ; 6 ; 8 ; 4 ; 7]; + % data.stim_type = {'delete'; 'delete'; 'keep'; 'keep' ; 'keep' ; 'delete'; 'delete'}; + % + % new_content = bids.transformers(transformers, data); + % + % new_content.trial_type + % ans = + % 3X1 cell array + % 'face' + % 'house' + % 'chair' + % + % new_content.stim_type + % ans = + % 3X1 cell array + % 'keep' + % 'keep' + % 'keep' + % + % new_content.onset + % ans = + % 1 + % 3 + % 7 + % + % new_content.duration + % ans = + % 2 + % 4 + % 2 + % + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + % TODO: tests to see if works on columns with mixed content (cell of numbers and strings) + % TODO: merge only if cell content matches some condition + + fields = fieldnames(data); + + if all(ismember(fields, {'onset', 'duration'})) + error('input data must have onset and duration fields.'); + end + + % sort data by onset + [~, idx] = sort(data.onset); + for i_field = 1:numel(fields) + data.(fields{i_field}) = data.(fields{i_field})(idx); + end + + input = bids.transformers_list.get_input(transformer, data); + + % create a new structure add a new row every time we encounter a new row + % with a different content (for the input of interest) from the previous one. + % + new_data = struct(); + row = 1; + + for i = 1:numel(input) + + if ~isfield(data, input{i}) + continue + end + + % start with values from the first row and start loop at row 2 + previous_value = data.(input{i})(1); + if iscell(previous_value) + previous_value = previous_value{1}; + end + + onset = data.onset(1); + + for j = 2:numel(data.(input{i})) + + this_value = data.(input{i})(j); + if iscell(this_value) + this_value = this_value{1}; + end + + is_same = compare_rows(this_value, previous_value); + + if ~is_same + + [new_data, row] = add_row(data, new_data, onset, row, j - 1); + + onset = data.onset(j); + + end + + previous_value = this_value; + + end + + % for the last row + new_data = add_row(data, new_data, onset, row, j); + + end + +end + +function is_same = compare_rows(this_value, previous_value) + + is_same = false; + + if isempty(previous_value) + elseif all(isnumeric([this_value, previous_value])) && this_value == previous_value + is_same = true; + elseif all(ischar([this_value, previous_value])) && strcmp(this_value, previous_value) + is_same = true; + end + +end + +function [new_data, new_data_row] = add_row(data, new_data, onset, new_data_row, data_row) + + fields = fieldnames(data); + + for i_field = 1:numel(fields) + new_data.(fields{i_field})(new_data_row, 1) = data.(fields{i_field})(data_row); + end + new_data.onset(new_data_row, 1) = onset; + new_data.duration(new_data_row, 1) = data.onset(data_row) + data.duration(data_row) - onset; + + new_data_row = new_data_row + 1; + +end diff --git a/+bids/+transformers_list/Product.m b/+bids/+transformers_list/Product.m new file mode 100644 index 00000000..cd166d4e --- /dev/null +++ b/+bids/+transformers_list/Product.m @@ -0,0 +1,89 @@ +function data = Product(transformer, data) + % + % Computes the row-wise product of two or more columns. + % + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "Product", + % "Input": ["duration", "reaction_time"], + % "Output": "duration_X_reaction_time", + % "OmitNan": false, + % } + % + % + % Arguments: + % + % :param Input: **mandatory**. Names of two or more columns to compute the product of. + % :type Input: array + % + % :param Output: **mandatory**. Name of the newly generated column. + % :type Output: string or array + % + % :param OmitNan: Optional. If ``false`` any column with nan values will return a nan value. + % If ``true`` nan values are skipped. Defaults to ``false``. + % :type OmitNan: logical + % + % + % **CODE EXAMPLE**:: + % + % transformer = struct('Name', 'Product', ... + % 'Input', {'duration', 'reaction_time'}, ... + % 'OmitNan', false, ... + % 'Ouput', 'duration_X_reaction_time'); + % + % + % data = bids.transformers(transformer, data); + % + % + % ans = TODO + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + input = bids.transformers_list.get_input(transformer, data); + if any(~ismember(input, fieldnames(data))) + return + end + + output = bids.transformers_list.get_output(transformer, data); + + assert(numel(output) == 1); + + output = output{1}; + + if isfield(transformer, 'OmitNan') + omit_nan = transformer.OmitNan; + else + omit_nan = false; + end + + tmp = []; + + for i = 1:numel(input) + + if ~isnumeric(data.(input{i})) + error('non numeric variable: %s', input{i}); + end + + if ~isempty(tmp) && length(tmp) ~= length(data.(input{i})) + error('trying to concatenate variables of different lengths: %s', input{i}); + end + + tmp(:, i) = data.(input{i}); + + end + + if omit_nan + data.(output) = prod(tmp, 2, 'omitnan'); + + else + data.(output) = prod(tmp, 2); + + end + +end diff --git a/+bids/+transformers_list/Rename.m b/+bids/+transformers_list/Rename.m new file mode 100644 index 00000000..a3daa71d --- /dev/null +++ b/+bids/+transformers_list/Rename.m @@ -0,0 +1,68 @@ +function data = Rename(transformer, data) + % + % Rename a variable. + % + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "Rename", + % "Input": [ + % "age_gt_70", + % "age_lt_18", + % ], + % "Output": [ + % "senior", + % "teenager", + % ] + % } + % + % + % Arguments: + % + % :param Input: **mandatory**. The name(s) of the variable(s) to rename. + % :type Input: string or array + % + % :param Output: Optional. New column names to output. + % Must match length of input column(s), + % and columns will be mapped 1-to-1 in order. + % :type Output: string or array + % + % + % **CODE EXAMPLE**:: + % + % transformer = struct('Name', 'Rename', ... + % 'Input', {{'age_gt_70', 'age_lt_18'}}, ... + % 'Ouput', {{'senior', 'teenager'}}); + % + % data.age_gt_70 = 75; + % + % data = bids.transformers(transformer, data); + % + % data. TODO + % + % ans = TODO + % + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + input = bids.transformers_list.get_input(transformer, data); + output = bids.transformers_list.get_output(transformer, data); + + assert(numel(input) == numel(output)); + + for i = 1:numel(input) + + if ~isfield(data, input{i}) + continue + end + + data.(output{i}) = data.(input{i}); + data = rmfield(data, input{i}); + end + +end diff --git a/+bids/+transformers_list/Replace.m b/+bids/+transformers_list/Replace.m new file mode 100644 index 00000000..b8a40fca --- /dev/null +++ b/+bids/+transformers_list/Replace.m @@ -0,0 +1,252 @@ +function data = Replace(transformer, data) + % + % Replaces values in one or more input columns. + % + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "Replace", + % "Input": [ + % "fruits", + % ], + % "Replace": [ + % {"key": "apple", "value": "bee"}, + % {"key": "elusive", "value": 5}, + % {"key": -1, "value": 0}] + % ], + % "Attribute": "all" + % } + % + % + % Arguments: + % + % :param Input: **mandatory**. Name(s of column(s) to search and replace within. + % :type Input: string or array + % + % :param Replace: **mandatory**. The mapping old values (``"key"``) to new values. + % (``"value"``). + % ``key`` can be a regular expression. + % :type Replace: array of objects + % + % :param Attribute: Optional. The column attribute to apply the replace to. + % :type Attribute: array + % + % Valid values include: + % + % - ``"value"`` (the default), + % - ``"duration"``, + % - ``"onset"``, + % - and ``"all"``. + % + % In the last case, all three attributes + % (``"value"``, ``"duration"``, and ``"onset"``) will be scanned. + % + % .. note: + % + % The rows of the ``attributes`` columns matching the ``key`` from the + % ``input`` will be replaced by ``value``. + % + % All replacemenets are done in sequentially. + % + % :param Output: Optional. Optional names of columns to output. + % Must match length of input column(s) if provided, + % and columns will be mapped 1-to-1 in order. + % If no output values are provided, + % the replacement transformation is applied in-place + % to all the inputs. + % :type Output: string or array + % + % + % **CODE EXAMPLE**:: + % + % + % data.fruits = {'apple'; 'banana'; 'elusive'}; + % data.onset = {1; 2; 3}; + % data.duration = {-1; 1; 3}; + % + % replace = struct('key', {'apple'; 'elusive'}, 'value', -1); + % replace(end+1).key = -1; + % replace(end).value = 0; + % + % transformer = struct('Name', 'Replace', ... + % 'Input', 'fruits', ... + % 'Attribute', 'all', ... + % 'Replace', replace); + % + % data = bids.transformers(transformer, data); + % + % + % data.fruits + % + % ans = + % 3X1 cell array + % [ 0] + % 'banana' + % [ 0] + % + % data.onset + % + % ans = + % 3x1 cell array + % [1] + % [2] + % [3] + % + % data.duration + % + % ans = + % 3X1 cell array + % [-1] + % [ 1] + % [ 3] + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + input = bids.transformers_list.get_input(transformer, data); + output = bids.transformers_list.get_output(transformer, data); + + attributes = get_attribute_to_replace(transformer); + + replace = transformer.Replace; + + for i = 1:numel(input) + + if ~isfield(data, input{i}) + continue + end + + for ii = 1:numel(replace) + + this_input = data.(input{i}); + + this_replace = replace(ii); + if iscell(this_replace) + this_replace = this_replace{1}; + end + + key = this_replace.key; + + if ischar(key) && iscellstr(this_input) + key = bids.internal.regexify(key); + idx = ~cellfun('isempty', regexp(this_input, key, 'match')); + + elseif isnumeric(key) && isnumeric(this_input) + idx = this_input == key; + + elseif ischar(key) && iscell(this_input) + idx = cellfun(@(x) ischar(x) && ~isempty(regexp(x, key, 'match')), this_input); + + elseif isnumeric(key) && iscell(this_input) + idx = cellfun(@(x) isnumeric(x) && x == key, this_input); + + else + continue + + end + + value = this_replace.value; + data = replace_for_attributes(data, attributes, output{i}, this_input, idx, value); + + end + + end + +end + +function [this_output, output] = get_this_output(data, attr, output, this_input) + + switch attr + + case 'value' + if isfield(data, output) + this_output = data.(output); + else + this_output = this_input; + end + + case {'onset', 'duration'} + output = attr; + this_output = data.(attr); + + end + +end + +function string = regexify(string) + % + % Turns a string into a simple regex. Useful to query bids dataset with + % bids.query that by default expects will treat its inputs as regex. + % + % Input --> Output + % + % ``foo`` --> ``^foo$`` + % + % USAGE:: + % + % string = regexify(string) + + if isempty(string) + string = '^$'; + return + end + if ~strcmp(string(1), '^') + string = ['^' string]; + end + if ~strcmp(string(end), '$') + string = [string '$']; + end +end + +function data = replace_for_attributes(data, attributes, output, this_input, idx, value) + + % loop over value, onset, duration + for i = 1:numel(attributes) + + [this_output, output] = get_this_output(data, attributes{i}, output, this_input); + + if isnumeric(this_output) + if ischar(value) + value = {value}; + this_output = num2cell(this_output); + end + this_output(idx) = repmat(value, sum(idx), 1); + + elseif iscellstr(this_output) + this_output(idx) = repmat({value}, sum(idx), 1); + + elseif iscell(this_output) + this_output(idx) = repmat({value}, sum(idx), 1); + + end + + data.(output) = this_output; + + end + +end + +function attributes = get_attribute_to_replace(transformer) + attributes = {'value'}; + if isfield(transformer, 'Attribute') + attributes = transformer.Attribute; + end + if ~ismember(attributes, {'value', 'onset', 'duration', 'all'}) + msg = sprintf(['Attribute must be one of ', ... + '"value", "onset", "duration" or "all" for Replace.\nGot: %s'], ... + char(attributes)); + bids.internal.error_handling(mfilename(), ... + 'invalidAttribute', ... + msg, ... + false); + end + if ~iscell(attributes) + attributes = {attributes}; + end + if strcmpi(attributes, 'all') + attributes = {'value', 'onset', 'duration'}; + end +end diff --git a/+bids/+transformers_list/Scale.m b/+bids/+transformers_list/Scale.m new file mode 100644 index 00000000..0008534a --- /dev/null +++ b/+bids/+transformers_list/Scale.m @@ -0,0 +1,131 @@ +function data = Scale(transformer, data) + % + % Scales the values of one or more columns. + % + % Semantics mimic scikit-learn, such that demeaning and + % rescaling are treated as independent arguments, + % with the default being to apply both + % (i.e., standardizing each value so that it has zero mean and unit SD). + % + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "Scale", + % "Input": "reaction_time", + % "Demean": true, + % "Rescale": true, + % "ReplaceNa": true, + % "Output": "scaled_reaction_time" + % } + % + % + % Arguments: + % + % :param Input: **mandatory**. Names of columns to standardize. + % :type Input: string or array + % + % :param Demean: Optional. If ``true``, subtracts the mean from each input column + % (i.e., applies mean-centering). + % :type Demean: logical + % + % :param Rescale: Optional. If ``true``, divides each column by its standard deviation. + % :type Rescale: logical + % + % :param ReplaceNa: Optional. Whether/when to replace missing values with 0. + % If ``"off"``, no replacement is performed. + % If ``"before"``, missing values are replaced with 0 before scaling. + % If ``"after"``, missing values are replaced with 0 after scaling. + % Defaults to ``"off"`` + % :type ReplaceNa: logical + % + % :param Output: Optional. Optional names of columns to output. + % Must match length of input column if provided, + % and columns will be mapped 1-to-1 in order. + % If no output values are provided, + % the scaling transformation is applied in-place to all the input. + % :type Output: string or array + % + % + % **CODE EXAMPLE**:: + % + % transformer = struct('Name', 'Scale', ... + % 'Input', 'reaction_time', + % 'Demean', true, + % 'Rescale', true, + % 'ReplaceNa', true, + % 'Output', 'scaled_reaction_time'); + % + % data.reaction_time = TODO + % + % data = bids.transformers(transformer, data); + % + % data.scaled_reaction_time = TODO + % + % ans = TODO + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + input = bids.transformers_list.get_input(transformer, data); + output = bids.transformers_list.get_output(transformer, data); + + demean = true; + if isfield(transformer, 'Demean') + demean = transformer.Demean; + end + + rescale = true; + if isfield(transformer, 'Rescale') + rescale = transformer.Rescale; + end + + replace_na = 'off'; + if isfield(transformer, 'ReplaceNa') + replace_na = transformer.ReplaceNa; + end + + for i = 1:numel(input) + + if ~isfield(data, input{i}) + continue + end + + this_input = data.(input{i}); + + if ~isnumeric(this_input) + error('non numeric variable: %s', input{i}); + end + + nan_values = isnan(this_input); + + if numel(unique(this_input)) == 1 && ismember(replace_na, {'off', 'before'}) + error(['Cannot scale a column with constant value %s\n', ... + 'If you want a constant column of 0 returned,\n' + 'set "replace_na" to "after"'], unique(this_input)); + end + + if strcmp(replace_na, 'before') + this_input(nan_values) = zeros(sum(nan_values)); + end + + if demean + this_input = this_input - mean(this_input, 'omitnan'); + end + + if rescale + this_input = this_input / std(this_input, 'omitnan'); + end + + if strcmp(replace_na, 'after') + this_input(nan_values) = zeros(sum(nan_values)); + end + + data.(output{i}) = this_input; + + end + +end diff --git a/+bids/+transformers_list/Select.m b/+bids/+transformers_list/Select.m new file mode 100644 index 00000000..440f3906 --- /dev/null +++ b/+bids/+transformers_list/Select.m @@ -0,0 +1,78 @@ +function data = Select(transformer, data) + % + % The select transformation specifies which columns to retain for subsequent analysis. + % + % Any columns that are not specified here will be dropped. + % + % The only exception is when dealing with data with ``onset`` and ``duration`` + % columns (from ``*_events.tsv`` files) in this case the onset and duration column + % are also automatically selected. + % + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "Select", + % "Input": [ + % "valid_trials", + % "reaction_time" + % ] + % } + % + % + % Arguments: + % + % :param Input: **mandatory**. The names of all columns to keep. + % Any columns not in this array will be deleted and + % will not be available to any subsequent transformations + % or downstream analyses. + % :type Input: string or array + % + % .. note:: + % + % one can think of select as the inverse the ``Delete`` transformation + % that removes all named columns from further analysis. + % + % + % **CODE EXAMPLE**:: + % + % + % transformer = struct('Name', 'Select', ... + % 'Input', {{'valid_trials', 'reaction_time'}}); + % + % data.valid_trials = TODO + % data.invalid_trials = TODO + % data.reaction_time = TODO + % data.onset = TODO + % data.duration = TODO + % + % data = bids.transformers(transformer, data); + % + % data = TODO + % + % ans = TODO + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + input = bids.transformers_list.get_input(transformer, data); + + for i = 1:numel(input) + + if ~isfield(data, input{i}) + continue + end + + tmp.(input{i}) = data.(input{i}); + end + + % also grab onset and duration for events + if bids.transformers_list.is_run_level(data) + tmp.onset = data.onset; + tmp.duration = data.duration; + end + + data = tmp; +end diff --git a/+bids/+transformers_list/Split.m b/+bids/+transformers_list/Split.m new file mode 100644 index 00000000..e7eeb374 --- /dev/null +++ b/+bids/+transformers_list/Split.m @@ -0,0 +1,198 @@ +function data = Split(transformer, data) + % + % Split a variable into N variables as defined by the levels of one or more other variables. + % + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "Split", + % "Input": [ + % "sex", + % ], + % "By": [ + % "sex_m", + % "age_gt_twenty" + % ] + % } + % + % + % Arguments: + % + % :param Input: **mandatory**. The name of the variable(s) to operate on. + % :type Input: array + % + % :param By: Optional. Name(s) for variable(s) to split on. + % :type By: array + % + % For example, for given a variable Condition + % that we wish to split on two categorical columns A and B, + % where a given row has values A=a and B=1, + % the generated name will be ``Condition_BY_A_a_BY_B_1``. + % + % + % **CODE EXAMPLE**:: + % + % transformer = struct('Name', 'Split', ... + % 'Input', 'sex', ... + % 'By', {{'sex_m', 'age_gt_twenty'}}); + % + % data.sex = TODO; + % data.sex_m = TODO; + % data.age_gt_twenty = TODO + % + % data = bids.transformers_list.split(transformer, data); + % + % data.sex_BY % TODO + % + % ans = TODO + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + % treat By as a stack + % + % work recursively + % + % - apply first element of By to all Input + % - we keep track of the new input that will be used for the next element of By + % - we keep track of which rows to keep for each original source input + % - we keep track of the source input through the recursions + + % We are done recursing. Do the actual splitting + if isempty(transformer.By) + + if ~isfield(transformer, 'rows_to_keep') + if isfield(transformer, 'verbose') + % in case user gave an empty By + warning('empty "By" field'); + end + return + end + + input = transformer.Input; + + % TODO + % output = bids.transformers_list.get_output(transformer, data); + + for i = 1:numel(input) + + if isfield(data, input{i}) + error('New field %s already exist in data.', input{i}); + end + + sourcefield = transformer.source{i}; + rows_to_keep = transformer.rows_to_keep{i}; + + if isnumeric(data.(sourcefield)) + tmp = nan(size(data.(sourcefield))); + else + tmp = repmat({nan}, size(data.(sourcefield))); + end + + tmp(rows_to_keep) = data.(sourcefield)(rows_to_keep); + data.(input{i}) = tmp; + + end + + return + + end + + transformer.By = sort(transformer.By); + + % initialise for recursion + if ~isfield(transformer, 'rows_to_keep') + + input = bids.transformers_list.get_input(transformer, data); + input = unique(input); + + if isempty(input) + return + end + + % make sure all variables to split by are there + bids.transformers_list.check_field(transformer.By, data, 'By'); + + transformer.source = input; + + % assume all rows are potentially ok at first + for i = 1:numel(input) + transformer.rows_to_keep{i} = ones(size(data.(input{i}))); + end + + else + + input = transformer.Input; + + end + + new_input = {}; + new_rows_to_keep = {}; + new_source = {}; + + % pop the stack + by = transformer.By{1}; + this_by = data.(by); + transformer.By(1) = []; + + % treat input as a queue + for i = 1:numel(input) + + % deal with nans + if iscell(this_by) + nan_values = cellfun(@(x) all(isnan(x)), this_by); + if any(nan_values) + this_by(nan_values) = repmat({'NaN'}, 1, sum(nan_values)); + end + end + + levels = unique(this_by); + + if isempty(levels) + continue + end + + for j = 1:numel(levels) + + if iscell(levels) + this_level = levels{j}; + else + this_level = levels(j); + end + + % create the new field name and make sure it is valid + if isnumeric(this_level) + field = [input{i} '_BY_' by '_' num2str(this_level)]; + else + field = [input{i} '_BY_' by '_' this_level]; + end + field = bids.transformers_list.coerce_fieldname(field); + + % store rows, source and input for next iteration + if strcmp(this_level, 'NaN') + new_rows_to_keep{end + 1} = all([transformer.rows_to_keep{i} ... + cellfun(@(x) all(isnan(x)), this_by)], ... + 2); + else + new_rows_to_keep{end + 1} = all([transformer.rows_to_keep{i} ... + ismember(this_by, this_level)], ... + 2); + end + new_source{end + 1} = transformer.source{i}; + new_input{end + 1} = field; + + end + + end + + transformer.Input = new_input; + transformer.rows_to_keep = new_rows_to_keep; + transformer.source = new_source; + + data = bids.transformers_list.Split(transformer, data); + +end diff --git a/+bids/+transformers_list/Std.m b/+bids/+transformers_list/Std.m new file mode 100644 index 00000000..d23db600 --- /dev/null +++ b/+bids/+transformers_list/Std.m @@ -0,0 +1,87 @@ +function data = Std(transformer, data) + % + % Compute the sample standard deviation. + % + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "Std", + % "Input": "reaction_time", + % "OmitNan": false, + % "Output": "std_RT" + % } + % + % + % Arguments: + % + % :param Input: **mandatory**. The name of the variable to operate on. + % :type Input: string or array + % + % :param OmitNan: Optional. If ``false`` any column with nan values will return a nan value. + % If ``true`` nan values are skipped. Defaults to ``false``. + % :type OmitNan: logical + % + % :param Output: Optional. The optional column names to write out to. + % By default, computation is done in-place (i.e., input columnise overwritten). + % :type Output: string or array + % + % + % **CODE EXAMPLE**:: + % + % transformer = struct('Name', 'Std', ... + % 'Input', 'reaction_time', ... + % 'OmitNan', false, ... + % 'Ouput', 'std_RT'); + % + % data.reaction_time = TODO + % + % data = bids.transformers(transformer, data); + % + % data.std_RT = TODO + % + % ans = TODO + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + overwrite = false; + + input = bids.transformers_list.get_input(transformer, data); + output = bids.transformers_list.get_output(transformer, data, overwrite); + + if ~isempty(output) + assert(numel(input) == numel(output)); + end + + if isfield(transformer, 'OmitNan') + omit_nan = transformer.OmitNan; + else + omit_nan = false; + end + + for i = 1:numel(input) + + if ~isfield(data, input{i}) + continue + end + + output_column = [input{i} '_std']; + if ~isempty(output) + output_column = output{i}; + end + + if omit_nan + data.(output_column) = std(data.(input{i}), 'omitnan'); + + else + data.(output_column) = std(data.(input{i})); + + end + + end + +end diff --git a/+bids/+transformers_list/Sum.m b/+bids/+transformers_list/Sum.m new file mode 100644 index 00000000..715338dd --- /dev/null +++ b/+bids/+transformers_list/Sum.m @@ -0,0 +1,108 @@ +function data = Sum(transformer, data) + % + % Computes the (optionally weighted) row-wise sums of two or more columns. + % + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "Sum", + % "Input": ["duration", "reaction_time"], + % "Output": "duration_X_reaction_time", + % "Weights": [1, 0.5], + % "OmitNan": false, + % } + % + % + % Arguments: + % + % :param Input: **mandatory**. Names of two or more columns to sum. + % :type Input: array + % + % :param Output: **mandatory**. Name of the newly generated column. + % :type Output: string or array + % + % :param OmitNan: Optional. If ``false`` any column with nan values will return a nan value. + % If ``true`` nan values are skipped. Defaults to ``false``. + % :type OmitNan: logical + % + % :param Weights: Optional. Optional array of floats giving the weights of the columns. + % If provided, length of weights must equal + % to the number of values in input, + % and weights will be mapped 1-to-1 onto named columns. + % If no weights are provided, + % defaults to unit weights (i.e., simple sum). + % :type Weights: array + % + % + % **CODE EXAMPLE**:: + % + % transformer = struct('Name', 'Sum', ... + % 'Input', {{'duration', 'reaction_time'}}, ... + % 'OmitNan', false, ... + % 'Weights': [1, 0.5], ... + % 'Ouput', 'duration_plus_reaction_time'); + % + % data.duration = TODO + % data.reaction_time = TODO + % + % data = bids.transformers(transformer, data); + % + % data.duration_plus_reaction_time = TODO + % + % ans = TODO + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + input = bids.transformers_list.get_input(transformer, data); + if any(~ismember(input, fieldnames(data))) + return + end + + output = bids.transformers_list.get_output(transformer, data); + + assert(numel(output) == 1); + + output = output{1}; + + if isfield(transformer, 'Weights') + weights = transformer.Weights; + else + weights = ones(size(input)); + end + + if isfield(transformer, 'OmitNan') + omit_nan = transformer.OmitNan; + else + omit_nan = false; + end + + tmp = []; + + for i = 1:numel(input) + + if ~isnumeric(data.(input{i})) + error('non numeric variable: %s', input{i}); + end + + if ~isempty(tmp) && length(tmp) ~= length(data.(input{i})) + error('trying to concatenate variables of different lengths: %s', input{i}); + end + + tmp(:, i) = data.(input{i}) * weights(i); + + end + + if omit_nan + data.(output) = sum(tmp, 2, 'omitnan'); + + else + data.(output) = sum(tmp, 2); + + end + +end diff --git a/+bids/+transformers_list/Threshold.m b/+bids/+transformers_list/Threshold.m new file mode 100644 index 00000000..20869dd3 --- /dev/null +++ b/+bids/+transformers_list/Threshold.m @@ -0,0 +1,127 @@ +function data = Threshold(transformer, data) + % + % Thresholds input values at a specified cut-off and optionally binarizes the result. + % + % + % **JSON EXAMPLE**: + % + % .. code-block:: json + % + % { + % "Name": "Threshold", + % "Input": "onset", + % "Threshold": 0.5, + % "Binarize": true, + % "Output": "delayed_onset" + % } + % + % + % Arguments: + % + % :param Input: **mandatory**. The name(s)of the column(s) to threshold/binarize. + % :type Input: string or array + % + % :param Threshold: Optional. The cut-off to use for thresholding. Defaults to ``0``. + % :type Threshold: float + % + % :param Binarize: Optional. If ``true``, thresholded values will be binarized + % (i.e., all non-zero values will be set to 1). + % Defaults to ``false``. + % :type Binarize: logical + % + % :param Above: Optional. Specifies which values to retain with respect to the cut-off. + % If ``true``, all value above the threshold will be kept; + % if ``false``, all values below the threshold will be kept. + % Defaults to ``true``. + % :type Above: logical + % + % :param Signed: Optional. Specifies whether to treat the threshold + % as signed (default) or unsigned. + % :type Signed: logical + % + % For example, when passing above=true and threshold=3, + % if signed=true, all and only values above +3 would be retained. + % If signed=false, all absolute values > 3 would be retained + % (i.e.,values in the range -3 < X < 3 would be set to 0). + % + % :param Output: Optional. Optional names of columns to output. + % Must match length of input column if provided, + % and columns will be mapped 1-to-1 in order. + % If no output values are provided, + % the threshold transformation is applied + % in-place to all the inputs. + % :type Output: string or array + % + % + % **CODE EXAMPLE**:: + % + % transformer = struct('Name', 'Threshold', ... + % 'Input', 'onset', ... + % 'Value', 3, ... + % 'Ouput', 'onset_minus_3'); + % + % data.onset = TODO + % + % data = bids.transformers(transformer, data); + % + % data.onset_minus_3 = TODO + % + % ans = TODO + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + input = bids.transformers_list.get_input(transformer, data); + output = bids.transformers_list.get_output(transformer, data); + + threshold = 0; + binarize = false; + above = true; + signed = true; + + if isfield(transformer, 'Threshold') + threshold = transformer.Threshold; + end + + if isfield(transformer, 'Binarize') + binarize = transformer.Binarize; + end + + if isfield(transformer, 'Above') + above = transformer.Above; + end + + if isfield(transformer, 'Signed') + signed = transformer.Signed; + end + + for i = 1:numel(input) + + if ~isfield(data, input{i}) + continue + end + + valuesToThreshold = data.(input{i}); + + if ~signed + valuesToThreshold = abs(valuesToThreshold); + end + + if above + idx = valuesToThreshold > threshold; + else + idx = valuesToThreshold < threshold; + end + + tmp = zeros(size(data.(input{i}))); + tmp(idx) = data.(input{i})(idx); + + if binarize + tmp(idx) = 1; + end + + data.(output{i}) = tmp; + end + +end diff --git a/+bids/+transformers_list/check_field.m b/+bids/+transformers_list/check_field.m new file mode 100644 index 00000000..39d1a0d5 --- /dev/null +++ b/+bids/+transformers_list/check_field.m @@ -0,0 +1,25 @@ +function check_field(field_list, data, field_type, tolerant) + % + % check that each field in field_list is present + % in the data structure + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + if nargin < 4 + tolerant = false; + end + + available_variables = fieldnames(data); + + available_from_fieldlist = ismember(field_list, available_variables); + + if ~all(available_from_fieldlist) + field_list = cellstr(field_list); + msg = sprintf('missing variable(s): "%s"', ... + strjoin(field_list(~available_from_fieldlist), '", "')); + bids.internal.error_handling(mfilename(), ['missing' field_type], msg, tolerant, true); + end + +end diff --git a/+bids/+transformers_list/coerce_fieldname.m b/+bids/+transformers_list/coerce_fieldname.m new file mode 100644 index 00000000..3ad478a5 --- /dev/null +++ b/+bids/+transformers_list/coerce_fieldname.m @@ -0,0 +1,10 @@ +function new_field = coerce_fieldname(field) + % + + % (C) Copyright 2022 BIDS-MATLAB developers + new_field = regexprep(field, '[^a-zA-Z0-9_]', ''); + if ~strcmp(new_field, field) + warning('Field "%s" renamed to "%s"', field, new_field); + end + +end diff --git a/+bids/+transformers_list/get_field_attribute.m b/+bids/+transformers_list/get_field_attribute.m new file mode 100644 index 00000000..2b07ff5d --- /dev/null +++ b/+bids/+transformers_list/get_field_attribute.m @@ -0,0 +1,28 @@ +function attr = get_field_attribute(data, field, type) + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + if nargin < 3 + type = {'value'}; + end + + switch type + + case 'value' + + attr = data.(field); + + case {'onset', 'duration'} + + attr = data.(type); + + otherwise + + bids.internal.error_handling(mfilename(), 'wrongAttribute', ... + 'Attribute must be one of "value", "onset", "duration"', ... + false); + + end + +end diff --git a/+bids/+transformers_list/get_input.m b/+bids/+transformers_list/get_input.m new file mode 100644 index 00000000..cfc2892d --- /dev/null +++ b/+bids/+transformers_list/get_input.m @@ -0,0 +1,46 @@ +function input = get_input(transformer, data) + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + assert(isstruct(transformer)); + assert(numel(transformer) == 1); + + verbose = true; + if isfield(transformer, 'verbose') + verbose = transformer.verbose; + end + + tolerant = true; + if isfield(transformer, 'tolerant') + tolerant = transformer.tolerant; + end + + if isfield(transformer, 'Input') + + input = transformer.Input; + + if isempty(input) + input = {}; + bids.internal.error_handling(mfilename(), ... + 'emptyInputField', ... + 'empty "Input" field', ... + tolerant, ... + verbose); + return + end + + else + input = {}; + return + + end + + if ~iscell(input) + input = {input}; + end + + bids.transformers_list.check_field(input, data, 'Input', tolerant); + +end diff --git a/+bids/+transformers_list/get_output.m b/+bids/+transformers_list/get_output.m new file mode 100644 index 00000000..90d2a60c --- /dev/null +++ b/+bids/+transformers_list/get_output.m @@ -0,0 +1,35 @@ +function output = get_output(transformer, data, overwrite) + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + if nargin < 3 + overwrite = true; + end + + if isfield(transformer, 'Output') && ~isempty(transformer.Output) + + output = transformer.Output; + + if ~iscell(output) + output = {output}; + end + + else + + % will overwrite input columns + if overwrite + input = bids.transformers_list.get_input(transformer, data); + output = input; + else + output = {}; + end + + end + + for i = 1:numel(output) + output{i} = bids.transformers_list.coerce_fieldname(output{i}); + end + +end diff --git a/+bids/+transformers_list/get_query.m b/+bids/+transformers_list/get_query.m new file mode 100644 index 00000000..9c0f523d --- /dev/null +++ b/+bids/+transformers_list/get_query.m @@ -0,0 +1,50 @@ +function [left, query_type, right] = get_query(transformer) + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + supported_types = {'>=', '<=', '==', '~=', '>', '<'}; + + if ~isfield(transformer, 'Query') || isempty(transformer.Query) + bids.internal.error_handling(mfilename(), 'emptyQuery', ... + 'empty query', ... + true); + left = ''; + query_type = ''; + right = ''; + return + + else + query = transformer.Query; + + end + + % should not happen because only one query is allowed + % but in case the user did input things into a cell + if iscell(query) + query = query{1}; + end + + % identify query type + for i = 1:numel(supported_types) + sts = strfind(query, supported_types{i}); + if ~isempty(sts) + query_type = supported_types{i}; + break + end + end + + if isempty(query_type) + bids.internal.error_handling(mfilename(), ... + 'unknownQueryType', ... + sprtinf(['Could not identify any of the supported types\n %s\n'... + 'in query %s'], supported_types, query), ... + false); + end + + tokens = regexp(query, query_type, 'split'); + left = strtrim(tokens{1}); + right = strtrim(tokens{2}); + +end diff --git a/+bids/+transformers_list/identify_rows.m b/+bids/+transformers_list/identify_rows.m new file mode 100644 index 00000000..0ec54da5 --- /dev/null +++ b/+bids/+transformers_list/identify_rows.m @@ -0,0 +1,72 @@ +function rows = identify_rows(data, left, query_type, right) + % + % USAGE:: + % + % rows = identify_rows(data, left, query_type, right) + % + % EXAMPLE:: + % + % transformer = struct('Name', 'Filter', ... + % 'Input', 'sex', ... + % 'Query', 'age > 20'); + % + % data.sex = {'M', 'F', 'F', 'M'}; + % data.age = [10, 21, 15, 26]; + % + % [left, query_type, right] = bids.transformers_list.get_query(transformer); + % rows = identify_rows(data, left, query_type, right); + % + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + if iscellstr(data.(left)) + + if ismember(query_type, {'>', '<', '>=', '<='}) + msg = sprtinf(['Types "%s" are not supported for queries on string\n'... + 'in query %s'], ... + {'>, <, >=, <='}, ... + query); + bids.internal.error_handling(mfilename(), ... + 'unsupportedQueryType', ... + msg, ... + false); + + end + + right = bids.internal.regexify(right); + rows = regexp(data.(left), right, 'match'); + rows = ~cellfun('isempty', rows); + if strcmp(query_type, '~=') + rows = ~rows; + end + + elseif isnumeric(data.(left)) + + right = str2num(right); + + switch query_type + + case '==' + rows = data.(left) == right; + + case '~=' + rows = data.(left) ~= right; + + case '>' + rows = data.(left) > right; + + case '<' + rows = data.(left) < right; + + case '>=' + rows = data.(left) >= right; + + case '<=' + rows = data.(left) <= right; + + end + + end +end diff --git a/+bids/+transformers_list/is_run_level.m b/+bids/+transformers_list/is_run_level.m new file mode 100644 index 00000000..75fe3af1 --- /dev/null +++ b/+bids/+transformers_list/is_run_level.m @@ -0,0 +1,14 @@ +function status = is_run_level(data) + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + status = false; + + fields = fieldnames(data); + + if all(ismember({'onset', 'duration'}, fields)) + status = true; + end + +end diff --git a/+bids/+transformers_list/miss_hit.cfg b/+bids/+transformers_list/miss_hit.cfg new file mode 100644 index 00000000..e621517b --- /dev/null +++ b/+bids/+transformers_list/miss_hit.cfg @@ -0,0 +1 @@ +regex_function_name: "[a-zA-Z]+(_[a-zA-Z0-9]+)*" diff --git a/+bids/+util/create_data_dict.m b/+bids/+util/create_data_dict.m new file mode 100644 index 00000000..be39ed1b --- /dev/null +++ b/+bids/+util/create_data_dict.m @@ -0,0 +1,307 @@ +function data_dict = create_data_dict(varargin) + % + % Create a JSON data dictionary for a TSV file. + % + % Levels in columns that may lead to invalid matlab structure fieldnames are + % renamed. Hence the output may need manual cleaning. + % + % Descriptions may be added for columns if a match is found in the BIDS + % schema: for example: trial_type, onset... + % + % To create better data dictionaries, please see the tools + % for `hierarchical event descriptions `_. + % + % USAGE:: + % + % data_dict = bids.util.create_data_dict(tsv_file, ... + % 'output', [], ... + % 'schema', true, ... + % 'force', false, ... + % 'level_limit', 10, ... + % 'verbose', true); + % + % + % :param output: filename for the output files. Can pass be a cell + % string of paths + % + % :param force: If set to ``false`` it will not overwrite any file already + % present in the destination. + % :type force: logical + % + % :param schema: If set to ``true`` it will use the schema to try to + % find definitions for the column headers + % :type schema: logical or a schema object + % + % :param level_limit: Maximum number of levels to list. Defaults to 10; + % :type level_limit: + % + % + % EXAMPLE:: + % + % BIDS = bids.layout(pth_bids_example, 'ds001')); + % + % tsv_files = bids.query(BIDS, 'data', ... + % 'sub', '01', ... + % 'suffix', 'events'); + % + % data_dict = bids.util.create_data_dict(tsv_files{1}, ... + % 'output', 'tmp.json', ... + % 'schema', true); + % + % + % + + % (C) Copyright 2021 Remi Gau + + default_schema = false; + default_output = []; + default_verbose = true; + default_force = false; + default_level_limit = 10; + + is_file_or_cellstr = @(x) (iscellstr(x) || exist(x, 'file')); + + args = inputParser(); + + addRequired(args, 'tsv_file', is_file_or_cellstr); + addParameter(args, 'level_limit', default_level_limit); + addParameter(args, 'output', default_output); + addParameter(args, 'schema', default_schema); + addParameter(args, 'verbose', default_verbose); + addParameter(args, 'force', default_force); + + parse(args, varargin{:}); + + tsv_file = args.Results.tsv_file; + level_limit = args.Results.level_limit; + output = args.Results.output; + schema = args.Results.schema; + force = args.Results.force; + verbose = args.Results.verbose; + + data_dict = struct(); + + if ~iscell(tsv_file) + tsv_file = {tsv_file}; + end + if isempty(tsv_file) + return + end + + content = get_content_from_tsv_files(tsv_file); + + headers = fieldnames(content); + + % keep track of modified levels to print them in a TSV at the end + modified_levels = struct('header', {{}}, ... + 'original_level_name', {{}}, ... + 'new_level_name', {{}}); + + for i = 1:numel(headers) + data_dict.(headers{i}) = set_dict(headers{i}, schema); + [data_dict, modified_levels] = add_levels_desc(data_dict, ... + headers{i}, ... + content, ... + level_limit, ... + modified_levels, ... + verbose); + end + + if isempty(output) + return + end + + if ~exist(output, 'file') || force + + bids.util.jsonwrite(output, data_dict); + + if ~isempty(modified_levels.header) + bids.util.tsvwrite(fullfile(fileparts(output), 'modified_levels.tsv'), ... + modified_levels); + end + + end + +end + +function content = get_content_from_tsv_files(tsv_file) + + content = bids.util.tsvread(tsv_file{1}); + + % if there is more than one TSV file, + % the content of all files is concatenated together + % to create a single data dictionary across TSVfiles. + if numel(tsv_file) > 1 + + for f = 2:numel(tsv_file) + + new_content = bids.util.tsvread(tsv_file{f}); + [content, new_content] = bids.internal.match_structure_fields(content, new_content); + + headers = fieldnames(content); + + for h = 1:numel(headers) + + append_to = content.(headers{h}); + to_append = new_content.(headers{h}); + + if isempty(append_to) + append_to = nan; + end + + if isempty(to_append) + to_append = nan; + end + + % dealing with nan + if iscellstr(append_to) && ~iscellstr(to_append) + if all(isnan(to_append)) + to_append = repmat({'n/a'}, numel(to_append), 1); + end + end + + if iscellstr(to_append) && ~iscellstr(append_to) + if all(isnan(append_to)) + append_to = repmat({'n/a'}, numel(append_to), 1); + end + end + + content.(headers{h}) = cat(1, append_to, to_append); + + % ??? + if (ischar(content.(headers{h})) || iscellstr(content.(headers{h}))) && ... + any(strcmp(content.(headers{h}), ' ')) + end + + end + + end + + end + +end + +function [json, modified] = add_levels_desc(json, hdr, tsv, lvl_limit, modified, verbose) + + levels = unique(tsv.(hdr)); + + % we do not list non integer numeric values + % as this is most likely not categorical + if numel(levels) > lvl_limit || ... + (isnumeric(levels) && not(all(isinteger(levels)))) + return + end + + json.(hdr).Levels = struct(); + + for i = 1:numel(levels) + + this_level = levels(i); + + if iscell(this_level) + this_level = this_level{1}; + end + + if strcmp(this_level, 'n/a') + continue + end + + level_name_before = this_level; + + % assume that numeric values (should be integers) are dummy coding for + % something categorical + % if not the user will have some clean up to do manually + if isnumeric(this_level) + % add a prefix because fieldnames cannot be numbers in matlab + this_level = ['level_' num2str(this_level)]; + end + + % remove any illegal character to turn it into a valid structure fieldname + this_level = regexprep(this_level, '[^a-zA-Z0-9]', '_'); + + pre = regexprep(this_level(1), '[0-9_]', ['level_' this_level(1)]); + if numel(this_level) > 1 + this_level = [pre this_level(2:end)]; + else + this_level = pre; + end + + if strcmp(this_level, '_') + bids.internal.error_handling(mfilename(), 'skippingLevel', ... + sprintf('\nSkipping level %s.', level_name_before), ... + true, ... + verbose); + continue + end + + if ~strcmp(level_name_before, this_level) + modified.header{end + 1} = hdr; + modified.original_level_name{end + 1} = level_name_before; + modified.new_level_name{end + 1} = this_level; + warning_modified_level_name(level_name_before, hdr, this_level, verbose); + end + + json.(hdr).Levels.(this_level) = 'TODO'; + + end + +end + +function warning_modified_level_name(level, header, new_name, verbose) + + tolerant = true; + + msg = sprintf(['Level "%s" of column "%s" modified to "%s".\n', ... + 'Check the HED tools to help you create better data dictionaries: %s.\n'], ... + level, header, new_name, ... + 'https://hedtools.ucsd.edu/hed/'); + + bids.internal.error_handling(mfilename(), 'modifiedLevel', msg, tolerant, verbose); +end + +function dict = set_dict(header, schema) + % + % get default description from the schema + % + + dict = default_dict(header); + + if (isobject(schema) && isfield(schema.content, 'objects')) || schema == true + + try + [def, status] = schema.get_definition(header); + catch + schema = bids.Schema(); + [def, status] = schema.get_definition(header); + end + + if ~status + return + end + + dict.LongName = def.name; + dict.Description = def.description; + + if isfield(def, 'unit') + dict.Units = def.unit; + elseif isfield(def, 'anyOf') + if iscell(def.anyOf) + number_allowed = cellfun(@(x) strcmp(x.type, 'number'), def.anyOf); + if any(number_allowed) && isfield(def.anyOf, 'unit') + dict.Units = def.anyOf{number_allowed}.unit; + end + end + end + + end + +end + +function default = default_dict(header) + + default = struct('LongName', header, ... + 'Description', 'TODO', ... + 'Units', 'TODO', ... + 'TermURL', 'TODO'); + +end diff --git a/+bids/+util/create_participants_tsv.m b/+bids/+util/create_participants_tsv.m new file mode 100644 index 00000000..0ee00153 --- /dev/null +++ b/+bids/+util/create_participants_tsv.m @@ -0,0 +1,86 @@ +function output_filename = create_participants_tsv(varargin) + % + % Creates a simple participants tsv for a BIDS dataset. + % + % + % USAGE:: + % + % output_filename = bids.util.create_participants_tsv(layout_or_path, ... + % 'use_schema', true, ... + % 'tolerant', true, ... + % 'verbose', false) + % + % + % :param layout_or_path: + % :type layout_or_path: path or structure + % + % :param use_schema: + % :type use_schema: logical + % + % :param tolerant: Set to ``true`` to turn validation errors into warnings + % :type tolerant: logical + % + % :param verbose: Set to ``true`` to get more feedback + % :type verbose: logical + % + + % (C) Copyright 2022 Remi Gau + + default_layout = pwd; + default_tolerant = true; + default_use_schema = true; + default_verbose = false; + + is_dir_or_struct = @(x) (isstruct(x) || isdir(x)); + is_logical = @(x) islogical(x); + + args = inputParser(); + + addOptional(args, 'layout_or_path', default_layout, is_dir_or_struct); + addParameter(args, 'tolerant', default_tolerant, is_logical); + addParameter(args, 'use_schema', default_use_schema, is_logical); + addParameter(args, 'verbose', default_verbose, is_logical); + + parse(args, varargin{:}); + + layout_or_path = args.Results.layout_or_path; + tolerant = args.Results.tolerant; + use_schema = args.Results.use_schema; + verbose = args.Results.verbose; + + %% + + layout = bids.layout(layout_or_path, 'use_schema', use_schema); + + if ~isempty(layout.participants) + msg = sprintf(['"participant.tsv" already exist for the following dataset. ', ... + 'Will not overwrite.\n', ... + '\t%s'], layout.pth); + bids.internal.error_handling(mfilename(), 'participantFileExist', msg, tolerant, verbose); + return + end + + subjects_list = bids.query(layout, 'subjects'); + % in case the query returns empty in case no file was indexed + if isempty(subjects_list) && ~use_schema + subjects_list = cellstr(bids.internal.file_utils('List', layout.pth, 'dir', '^sub-.*$')); + end + + subjects_list = [repmat('sub-', numel(subjects_list), 1), char(subjects_list')]; + + output_structure = struct('participant_id', subjects_list); + + output_filename = fullfile(layout.pth, 'participants.tsv'); + + bids.util.tsvwrite(fullfile(layout.pth, 'participants.tsv'), output_structure); + + if verbose + fprintf(1, ['\nCreated "participants.tsv" in the dataset.', ... + '\n\t%s\n', ... + 'Please add participant age, gender...\n', ... + 'See this section of the BIDS specification:\n\t%s\n'], ... + layout.pth, ... + bids.internal.url('participants')); + end + +end diff --git a/+bids/+util/create_readme.m b/+bids/+util/create_readme.m new file mode 100644 index 00000000..d2bd5a41 --- /dev/null +++ b/+bids/+util/create_readme.m @@ -0,0 +1,78 @@ +function create_readme(varargin) + % + % Create a README in a BIDS dataset. + % + % + % USAGE:: + % + % bids.util.create_readme(layout_or_path, is_datalad_ds, ... + % 'tolerant', true, ... + % 'verbose', false) + % + % + % :param layout_or_path: + % :type layout_or_path: path or structure + % + % :param tolerant: Set to ``true`` to turn validation errors into warnings + % :type tolerant: logical + % + % :param verbose: Set to ``true`` to get more feedback + % :type verbose: logical + % + + % (C) Copyright 2022 Remi Gau + + default_layout = pwd; + default_tolerant = true; + default_verbose = false; + + is_dir_or_struct = @(x) (isstruct(x) || isdir(x)); + is_logical = @(x) islogical(x); + + args = inputParser(); + + addOptional(args, 'layout_or_path', default_layout, is_dir_or_struct); + addOptional(args, 'is_datalad_ds', default_layout, is_logical); + addParameter(args, 'tolerant', default_tolerant, is_logical); + addParameter(args, 'verbose', default_verbose, is_logical); + + parse(args, varargin{:}); + + layout_or_path = args.Results.layout_or_path; + is_datalad_ds = args.Results.is_datalad_ds; + tolerant = args.Results.tolerant; + verbose = args.Results.verbose; + + pth = layout_or_path; + if isstruct(layout_or_path) + if isfield(layout_or_path, 'pth') + pth = layout_or_path.pth; + else + msg = 'Input structure is not a bids layout. Run bids.layout first.'; + bids.internal.error_handling(mfilename(), 'notBidsDatasetLayout', ... + msg, ... + tolerant, ... + verbose); + end + end + + readme_present = bids.internal.file_utils('List', pth, 'README.*|readme.*'); + if ~isempty(readme_present) + msg = sprintf('Dataset %s already contains a layout:\n\t%s\nWill not overwrite.\n', ... + pth); + bids.internal.error_handling(mfilename(), 'readmeAlreadyPresent', ... + msg, ... + tolerant, ... + verbose); + return + end + + %% + pth_to_readmes = fullfile(bids.internal.root_dir(), 'templates'); + src = fullfile(pth_to_readmes, 'README.template'); + if is_datalad_ds + src = fullfile(pth_to_readmes, 'README_datalad.template'); + end + + copyfile(src, fullfile(pth, 'README.md')); +end diff --git a/+bids/+util/create_scans_tsv.m b/+bids/+util/create_scans_tsv.m new file mode 100644 index 00000000..e0e1f2aa --- /dev/null +++ b/+bids/+util/create_scans_tsv.m @@ -0,0 +1,145 @@ +function output_filenames = create_scans_tsv(varargin) + % + % Create a simple scans.tsv for each participant of a BIDS dataset. + % + % + % USAGE:: + % + % output_filename = bids.util.create_scans_tsv(layout_or_path, ... + % 'use_schema', true, ... + % 'tolerant', true, ... + % 'verbose', false) + % + % + % :param layout_or_path: + % :type layout_or_path: path or structure + % + % :param use_schema: + % :type use_schema: logical + % + % :param tolerant: Set to ``true`` to turn validation errors into warnings + % :type tolerant: logical + % + % :param verbose: Set to ``true`` to get more feedback + % :type verbose: logical + % + + % (C) Copyright 2022 Remi Gau + + default_layout = pwd; + default_tolerant = true; + default_use_schema = true; + default_verbose = false; + + is_dir_or_struct = @(x) (isstruct(x) || isdir(x)); + is_logical = @(x) islogical(x); + + args = inputParser(); + + addOptional(args, 'layout_or_path', default_layout, is_dir_or_struct); + addParameter(args, 'tolerant', default_tolerant, is_logical); + addParameter(args, 'use_schema', default_use_schema, is_logical); + addParameter(args, 'verbose', default_verbose, is_logical); + + parse(args, varargin{:}); + + layout_or_path = args.Results.layout_or_path; + tolerant = args.Results.tolerant; + use_schema = args.Results.use_schema; + verbose = args.Results.verbose; + + % we only include recorindg files in the scans.tsv + sc = bids.Schema(); + suffixes = fieldnames(sc.content.objects.suffixes); + suffixes = setdiff(suffixes, {'aslcontext', ... + 'asllabeling', ... + 'channels', ... + 'coordsystem', ... + 'electrodes', ... + 'events', ... + 'headshape', ... + 'markers', ... + 'scans', ... + 'sessions', ... + 'stim'}); + + extension_group = fieldnames(sc.content.objects.extensions); + for i = 1:numel(extension_group) + extensions{i} = sc.content.objects.extensions.(extension_group{i}).value; %#ok<*AGROW> + end + + extensions = setdiff(extensions, {'.bval', ... + '.bvec', ... + '.jpg', ... + '.json', ... + '.txt'}); + %% + output_filenames = {}; + + layout = bids.layout(layout_or_path, 'use_schema', use_schema); + + subjects_list = bids.query(layout, 'subjects'); + + for i_sub = 1:numel(subjects_list) + + sessions_list = bids.query(layout, 'sessions', 'sub', subjects_list{i_sub}); + if isempty(sessions_list) + sessions_list = {''}; + end + + for i_ses = 1:numel(sessions_list) + + scans_file = fullfile(['sub-' subjects_list{i_sub}], ... + ['sub-' subjects_list{i_sub} '_scans.tsv']); + session_str = ''; + if ~isempty(sessions_list{i_ses}) + session_str = ['ses-' sessions_list{i_ses}]; + scans_file = fullfile(['sub-' subjects_list{i_sub}], ... + session_str, ... + ['sub-' subjects_list{i_sub}, ... + '_ses-' sessions_list{i_ses}, ... + '_scans.tsv']); + end + + if exist(fullfile(layout.pth, scans_file), 'file') + msg = sprintf(['"scans.tsv" %s already exist for the following dataset.', ... + 'Will not overwrite.\n', ... + '\t%s'], scans_file, layout.pth); + bids.internal.error_handling(mfilename(), 'scansFileExist', msg, true, verbose); + continue + end + + data = bids.query(layout, 'data', 'sub', subjects_list{i_sub}, ... + 'ses', sessions_list{i_ses}, ... + 'suffix', suffixes, ... + 'extension', extensions); + + for i_file = 1:numel(data) + data{i_file} = strrep(data{i_file}, ... + fullfile(layout.pth, ... + ['sub-' subjects_list{i_sub}], ... + [session_str filesep]), ... + ''); + end + + content = struct('filename', {data}, ... + 'acq_time', {cell(numel(data), 1)}, ... + 'comments', {cell(numel(data), 1)}); + + output_filenames{end + 1} = scans_file; %#ok + + bids.util.tsvwrite(fullfile(layout.pth, scans_file), content); + + end + end + + if verbose && ~isempty(output_filenames) + fprintf(1, ['\nCreated "scans.tsv" in the dataset.', ... + '\n\t%s\n', ... + 'Please add any extra information manually.\n', ... + 'See this section of the BIDS specification:\n\t%s\n'], ... + bids.internal.create_unordered_list(output_filenames), ... + bids.internal.url('scans')); + end + +end diff --git a/+bids/+util/create_sessions_tsv.m b/+bids/+util/create_sessions_tsv.m new file mode 100644 index 00000000..414a7a26 --- /dev/null +++ b/+bids/+util/create_sessions_tsv.m @@ -0,0 +1,118 @@ +function output_filenames = create_sessions_tsv(varargin) + % + % Create a simple sessions.tsv for each participant of a BIDS dataset. + % + % + % USAGE:: + % + % output_filename = bids.util.create_sessions_tsv(layout_or_path, ... + % 'use_schema', true, ... + % 'tolerant', true, ... + % 'verbose', false) + % + % + % :param layout_or_path: + % :type layout_or_path: path or structure + % + % :param use_schema: + % :type use_schema: logical + % + % :param tolerant: Set to ``true`` to turn validation errors into warnings + % :type tolerant: logical + % + % :param verbose: Set to ``true`` to get more feedback + % :type verbose: logical + % + + % (C) Copyright 2022 Remi Gau + + default_layout = pwd; + default_tolerant = true; + default_use_schema = true; + default_verbose = false; + + is_dir_or_struct = @(x) (isstruct(x) || isdir(x)); + is_logical = @(x) islogical(x); + + args = inputParser(); + + addOptional(args, 'layout_or_path', default_layout, is_dir_or_struct); + addParameter(args, 'tolerant', default_tolerant, is_logical); + addParameter(args, 'use_schema', default_use_schema, is_logical); + addParameter(args, 'verbose', default_verbose, is_logical); + + parse(args, varargin{:}); + + layout_or_path = args.Results.layout_or_path; + tolerant = args.Results.tolerant; + use_schema = args.Results.use_schema; + verbose = args.Results.verbose; + + %% + output_filenames = {}; + + layout = bids.layout(layout_or_path, 'use_schema', use_schema); + + sessions_list = bids.query(layout, 'sessions'); + if isempty(sessions_list) && use_schema + msg = sprintf(['There are no session in this dataset:\n', ... + '\t%s'], layout.pth); + bids.internal.error_handling(mfilename(), 'noSessionInDataset', msg, tolerant, verbose); + return + end + + subjects_list = bids.query(layout, 'subjects'); + % in case the query returns empty in case no file was indexed + if isempty(subjects_list) && ~use_schema + subjects_list = cellstr(bids.internal.file_utils('List', layout.pth, 'dir', '^sub-.*$')); + subjects_list = regexprep(subjects_list, 'sub-', ''); + end + + for i_sub = 1:numel(subjects_list) + + sessions_file = fullfile(['sub-' subjects_list{i_sub}], ... + ['sub-' subjects_list{i_sub} '_sessions.tsv']); + + if exist(sessions_file, 'file') + msg = sprintf(['"sessions.tsv" %s already exist for the following dataset.', ... + 'Will not overwrite.\n', ... + '\t%s'], sessions_file, layout.pth); + bids.internal.error_handling(mfilename(), 'sessionsFileExist', msg, true, verbose); + continue + end + + sessions_list = bids.query(layout, 'sessions', 'sub', subjects_list{i_sub}); + % in case the query returns empty in case no file was indexed + if isempty(sessions_list) && ~use_schema + sessions_list = cellstr(bids.internal.file_utils('List', ... + fullfile(layout.pth, ... + ['sub-' subjects_list{i_sub}]), ... + 'dir', ... + '^sub-.*$')); + sessions_list = regexprep(sessions_list, 'ses-', ''); + end + + if isempty(sessions_list) + continue + end + + sessions_list = [repmat('ses-', numel(sessions_list), 1), char(sessions_list')]; + content = struct('session_id', {sessions_list}, ... + 'acq_time', nan(size(sessions_list, 1), 1), ... + 'comments', nan(size(sessions_list, 1), 1)); + + output_filenames{end + 1} = sessions_file; %#ok + + bids.util.tsvwrite(fullfile(layout.pth, sessions_file), content); + end + + if verbose && ~isempty(output_filenames) + fprintf(1, ['\nCreated the following "sessions.tsv" in the dataset.', ... + '\n\t%s\n', ... + 'Please add any extra information manually.\n', ... + 'See this section of the BIDS specification:\n\t%s\n'], ... + bids.internal.create_unordered_list(output_filenames), ... + bids.internal.url('sessions')); + end + +end diff --git a/+bids/+util/download_ds.m b/+bids/+util/download_ds.m index 5a7bb3ef..edcc94de 100644 --- a/+bids/+util/download_ds.m +++ b/+bids/+util/download_ds.m @@ -8,7 +8,8 @@ % 'demo', 'moae', ... % 'out_path', fullfile(bids.internal.root_dir(), 'demos'), ... % 'force', false, ... - % 'verbose', true); + % 'verbose', true, ... + % 'delete_previous', true); % % % SPM:: @@ -45,6 +46,7 @@ % ftp://neuroimage.usc.edu/pub/tutorials/sample_omega.zip % % + % (C) Copyright 2021 BIDS-MATLAB developers % TODO @@ -59,67 +61,69 @@ default_out_path = ''; default_force = false; default_verbose = true; + default_delete_previous = false; - p = inputParser; + args = inputParser; - addParameter(p, 'source', default_source, @ischar); - addParameter(p, 'demo', default_demo, @ischar); - addParameter(p, 'out_path', default_out_path, @ischar); - addParameter(p, 'force', default_force); - addParameter(p, 'verbose', default_verbose); + addParameter(args, 'source', default_source, @ischar); + addParameter(args, 'demo', default_demo, @ischar); + addParameter(args, 'out_path', default_out_path, @ischar); + addParameter(args, 'delete_previous', default_delete_previous, @islogical); + addParameter(args, 'force', default_force, @islogical); + addParameter(args, 'verbose', default_verbose, @islogical); - parse(p, varargin{:}); + parse(args, varargin{:}); - verbose = p.Results.verbose; + verbose = args.Results.verbose; - out_path = p.Results.out_path; + out_path = args.Results.out_path; if isempty(out_path) out_path = fullfile(bids.internal.root_dir, 'demos'); - out_path = fullfile(out_path, p.Results.source, p.Results.demo); + out_path = fullfile(out_path, args.Results.source, args.Results.demo); elseif ~exist(out_path, 'dir') bids.util.mkdir(out_path); end % clean previous runs if isdir(out_path) - if p.Results.force - rmdir(out_path, 's'); + if args.Results.force + if args.Results.delete_previous + rmdir(out_path, 's'); + end else bids.internal.error_handling(mfilename(), 'dataAlreadyHere', ... ['The dataset is already present.' ... 'Use "force, true" to overwrite.'], ... true, verbose); - return end end - if strcmp(p.Results.source, 'spm') - if strcmp(p.Results.demo, 'facerep') - bids.internal.ds_spm_face_rep(fileparts(out_path)); - return - end - end - - [URL] = get_URL(p.Results.source, p.Results.demo, verbose); + [URL] = get_URL(args.Results.source, args.Results.demo, verbose); filename = bids.internal.download(URL, bids.internal.root_dir(), verbose); % Unzipping dataset [~, basename, ext] = fileparts(filename); if strcmp(ext, '.zip') - msg = sprintf('Unzipping dataset:\n %s\n\n', filename); + msg = sprintf('Unzipping dataset:\n %s to \n %s \n\n', ... + bids.internal.format_path(filename), ... + bids.internal.format_path(out_path)); print_to_screen(msg, verbose); - unzip(filename); + unzip(filename, out_path); delete(filename); switch basename case 'MoAEpilot.bids' - movefile('MoAEpilot', fullfile(out_path)); + copyfile(fullfile(out_path, 'MoAEpilot', '*'), out_path); + rmdir(fullfile(out_path, 'MoAEpilot'), 's'); + case 'face_rep' + copyfile(fullfile(out_path, basename, '*'), out_path); + rmdir(fullfile(out_path, basename), 's'); case 'multimodal_eeg' - movefile('EEG', fullfile(out_path)); + copyfile(fullfile(bids.internal.root_dir, 'EEG', '*'), out_path); otherwise - movefile(basename, fullfile(out_path)); + movefile(fullfile(bids.internal.root_dir, basename), out_path); end end @@ -129,7 +133,7 @@ function [URL, ftp_server, demo_path] = get_URL(source, demo, verbose) sources = {'spm', 'brainstorm'}; - demos = {'moae', 'facep', 'eeg', ... + demos = {'moae', 'facerep', 'eeg', ... 'ieeg', 'ecog', 'meg', 'meg_rest'}; switch source @@ -158,6 +162,9 @@ case 'eeg' demo_path = '/mmfaces/multimodal_eeg.zip'; + case 'facerep' + demo_path = '/face_rep/face_rep.zip'; + % brainstorm case 'ieeg' demo_path = '/pub/tutorials/tutorial_epimap_bids.zip'; diff --git a/+bids/+util/jsondecode.m b/+bids/+util/jsondecode.m index fec677c9..832f1fba 100644 --- a/+bids/+util/jsondecode.m +++ b/+bids/+util/jsondecode.m @@ -22,8 +22,8 @@ % :returns: - :json: JSON structure % % + % (C) Copyright 2018 Guillaume Flandin, Wellcome Centre for Human Neuroimaging - % % (C) Copyright 2018 BIDS-MATLAB developers persistent has_jsondecode diff --git a/+bids/+util/jsonencode.m b/+bids/+util/jsonencode.m index ff024ae3..968d2205 100644 --- a/+bids/+util/jsonencode.m +++ b/+bids/+util/jsonencode.m @@ -31,8 +31,8 @@ % - ``ConvertInfAndNaN``: encode ``NaN``, ``Inf`` and ``-Inf`` as ``"null"``; % [Default: ``true``] % + % (C) Copyright 2018 Guillaume Flandin, Wellcome Centre for Human Neuroimaging - % % (C) Copyright 2018 BIDS-MATLAB developers if ~nargin diff --git a/+bids/+util/jsonwrite.m b/+bids/+util/jsonwrite.m index 96068088..60f426eb 100644 --- a/+bids/+util/jsonwrite.m +++ b/+bids/+util/jsonwrite.m @@ -39,6 +39,7 @@ % - `JSON Standard `_ % - `jsonencode `_ % + % (C) Copyright 2018 Guillaume Flandin, Wellcome Centre for Human Neuroimaging % % $Id: spm_jsonwrite.m 8031 2020-12-10 13:37:00Z guillaume $ @@ -99,7 +100,9 @@ else fid = fopen(filename, 'wt'); if fid == -1 - error('Unable to open file "%s" for writing.', filename); + error(['Unable to open file "%s" for writing.', ... + '\nIf you are using a datalad dataset, make sure the file is unlocked.'], ... + bids.internal.file_utils(filename, 'cpath')); end fprintf(fid, '%s', S); fclose(fid); diff --git a/+bids/+util/mkdir.m b/+bids/+util/mkdir.m index 4f4e19d2..f39f946b 100644 --- a/+bids/+util/mkdir.m +++ b/+bids/+util/mkdir.m @@ -22,8 +22,10 @@ % Based on spm_mkdir from SPM12 % % + % (C) Copyright 2017-2021 Guillaume Flandin, Wellcome Centre for Human Neuroimaging % + % (C) Copyright 2018 BIDS-MATLAB developers sts = true; diff --git a/+bids/+util/plot_events.m b/+bids/+util/plot_events.m new file mode 100644 index 00000000..59289ce7 --- /dev/null +++ b/+bids/+util/plot_events.m @@ -0,0 +1,309 @@ +function plot_events(varargin) + % + % USAGE:: + % + % plot_events(events_files, 'include', include, ... + % 'trial_type_col', 'trial_type', ... + % 'model_file', path_to_model) + % + % :param events_files: BIDS events TSV files. + % :type events_files: path or cellstr of paths + % + % :param include: Optional. Restrict conditions to plot. + % :type include: char or cellstr + % + % :param trial_type_col: Optional. Defines the column where trial types are + % listed. Defaults to 'trial_type' + % :type trial_type_col: char or cellstr + % + % :param model_file: Optional. Bids stats model file to apply to events.tsv + % before plotting + % :type model_file: fullpath + % + % EXAMPLE:: + % + % data_dir = fullfile(get_test_data_dir(), 'ds108'); + % + % BIDS = bids.layout(data_dir); + % + % events_files = bids.query(BIDS, ... + % 'data', ... + % 'sub', '01', ... + % 'run', '01', ... + % 'suffix', 'events'); + % + % include = {'Reapp_Neg_Cue', 'Look_Neg_Cue', 'Look_Neutral_Cue'}; + % bids.util.plot_events(events_files, 'include', include); + % + % + + % (C) Copyright 2020 Remi Gau + + args = inputParser(); + + file_or_cellstring = @(x) (iscellstr(x) || exist(x, 'file')); + empty_or_file = @(x) (isempty(x) || exist(x, 'file')); + char_or_cellstring = @(x) (ischar(x) || iscellstr(x)); + + addRequired(args, 'events_files', file_or_cellstring); + addParameter(args, 'include', {}, char_or_cellstring); + addParameter(args, 'trial_type_col', 'trial_type', @ischar); + addParameter(args, 'model_file', '', empty_or_file); + + parse(args, varargin{:}); + + events_files = args.Results.events_files; + include = args.Results.include; + trial_type_col = args.Results.trial_type_col; + model_file = args.Results.model_file; + + if ischar(include) + include = {include}; + end + + if ischar(events_files) + events_files = {events_files}; + end + + bm = ''; + if ~isempty(model_file) + bm = bids.Model('file', model_file, 'verbose', true); + end + + for i = 1:numel(events_files) + plot_this_file(events_files{i}, include, trial_type_col, bm); + end + +end + +function plot_this_file(this_file, include, trial_type_col, bm) + + % From colorbrewer + % http://colorbrewer2.org/ + COLORS = [166, 206, 227 + 31, 120, 180 + 178, 223, 138 + 51, 160, 44 + 251, 154, 153 + 227, 26, 28 + 253, 191, 111 + 255, 127, 0 + 202, 178, 214 + 106, 61, 154 + 255, 255, 153 + 177, 89, 40]; + + tsv_content = bids.util.tsvread(this_file); + + matrix = {}; + data = tsv_content; + if ~isempty(bm) + [~, root_node_name] = bm.get_root_node(); + transformers = bm.get_transformations('Name', root_node_name); + matrix = bm.get_design_matrix('Name', root_node_name); + data = bids.transformers(transformers.Instructions, tsv_content); + end + + bids_file = bids.File(this_file); + + data = get_events_data(data, trial_type_col, include, matrix); + + [nb_col, nb_rows, subplot_grid] = return_figure_spec(tsv_content, data); + + % ensure we have enough colors for all conditions + COLORS = repmat(COLORS, ceil(nb_rows / size(COLORS, 1)), 1); + + fig_name = strrep(bids_file.filename, '_', ' '); + fig_name = strrep(fig_name, 'events.tsv', ' '); + figure('name', fig_name, ... + 'position', [50 50 2000 1000]); + + for iCdt = 1:nb_rows + + this_color = COLORS(iCdt, :) / 255; + + onset = data(iCdt).onset; + + duration = data(iCdt).duration; + + %% Time course + subplot(nb_rows, nb_col, subplot_grid{iCdt, 1}); + + hold on; + + if all(duration == 0) + + stem(onset, ones(1, numel(onset)), 'linecolor', this_color); + + else + + for iStim = 1:numel(onset) + + rectangle('position', [onset(iStim) 0 duration(iStim) 1], ... + 'FaceColor', this_color, ... + 'EdgeColor', this_color); + end + + end + + response_time = data(iCdt).response_time; + plot_response_time(response_time, onset); + + ylabel(sprintf(strrep(data(iCdt).name, '_', '\n'))); + + %% Duration distribution + subplot(nb_rows, nb_col, subplot_grid{iCdt, 2}); + plot_histogram(diff(onset), this_color); + + %% Response time distribution + has_response = ~isnan(response_time); + if any(has_response) + subplot(nb_rows, nb_col, subplot_grid{iCdt, 3}); + plot_histogram(response_time(has_response), this_color); + end + + end + + %% Update axis + xMin = floor(min(cat(1, data.onset))) - 1; + xMax = ceil(max(cat(1, data.onset) + cat(1, data.duration))); + xMax = xMax + 5; + + yMin = 0; + yMax = 1.1; + + for iCdt = 1:nb_rows + + subplot(nb_rows, nb_col, subplot_grid{iCdt, 1}); + + axis([xMin xMax yMin yMax]); + + % x tick in minutes + set(gca, ... + 'xTick', 0:60:xMax, ... + 'xTickLabel', '', ... + 'TickDir', 'out'); + end + + subplot(nb_rows, nb_col, subplot_grid{1, 1}); + title(fig_name); + + subplot(nb_rows, nb_col, subplot_grid{end, 1}); + set(gca, ... + 'xTick', 0:60:xMax, ... + 'xTickLabel', 0:60:xMax, ... + 'TickDir', 'out'); + xlabel('seconds'); + + subplot(nb_rows, nb_col, subplot_grid{1, 2}); + title('ISI distribution'); + + subplot(nb_rows, nb_col, subplot_grid{end, 2}); + xlabel('seconds'); + + if isfield(tsv_content, 'response_time') + + subplot(nb_rows, nb_col, subplot_grid{end, 3}); + xlabel('seconds'); + + subplot(nb_rows, nb_col, subplot_grid{1, 3}); + title('response time distribution'); + + end + +end + +function [nb_col, nb_rows, subplot_grid] = return_figure_spec(tsv_content, data) + + nb_rows = numel(data); + + nb_col = 8; + subplot_col_1 = 1:(nb_col - 1); + subplot_col_2 = nb_col; + subplot_col_3 = nan; + + if isfield(tsv_content, 'response_time') + nb_col = 9; + subplot_col_1 = 1:(nb_col - 2); + subplot_col_2 = nb_col - 1; + subplot_col_3 = nb_col; + end + + subplot_grid = {subplot_col_1, subplot_col_2, subplot_col_3}; + + for iCdt = 2:nb_rows + subplot_grid{iCdt, 1} = subplot_grid{iCdt - 1, 1} + +nb_col; + subplot_grid{iCdt, 2} = subplot_grid{iCdt - 1, 2} + +nb_col; + subplot_grid{iCdt, 3} = subplot_grid{iCdt - 1, 3} + +nb_col; + end + +end + +function data = get_events_data(data, trial_type_col, include, matrix) + + % TODO deal with events.tsv with only onset and duration + trial_type = data.(trial_type_col); + trial_type_list = unique(trial_type); + + if isempty(matrix) + for iCdt = 1:numel(trial_type_list) + matrix{iCdt} = [trial_type_col '.' trial_type_list{iCdt}]; + end + end + + tmp = struct('name', '', 'onset', [], 'duration', [], 'response_time', []); + + counter = 1; + + for i = 1:numel(matrix) + + if ~ischar(matrix{i}) + continue + end + + tokens = strsplit(matrix{i}, '.'); + if numel(tokens) ~= 2 + continue + end + + if ~isempty(include) && ~ismember(tokens{2}, include) + continue + end + + idx = strcmp(data.(tokens{1}), tokens{2}); + + tmp(counter).name = tokens{2}; + tmp(counter).onset = data.onset(idx); + tmp(counter).duration = data.duration(idx); + tmp(counter).response_time = nan(size(tmp(counter).onset)); + if isfield(data, 'response_time') + tmp(counter).response_time = data.response_time(idx); + end + + counter = counter + 1; + end + + data = tmp; + +end + +function plot_response_time(response_time, onset) + response_time = onset + response_time; + has_response = ~isnan(response_time); + if any(has_response) + stem(response_time(has_response), 0.5 * ones(1, sum(has_response)), 'k'); + end +end + +function plot_histogram(values, this_color) + hold on; + + hist(values, 20, 1); + h = findobj(gca, 'Type', 'patch'); + set(h, 'FaceColor', this_color); + set(h, 'EdgeColor', 'w'); + + ax = axis; + plot([0 0], [ax(3) ax(4)], 'k'); + plot([ax(1) ax(2)], [0 0], 'k'); +end diff --git a/+bids/+util/tsvread.m b/+bids/+util/tsvread.m index 37599beb..b967512d 100644 --- a/+bids/+util/tsvread.m +++ b/+bids/+util/tsvread.m @@ -14,7 +14,7 @@ % :type field_to_return: % % :param hdr: detect the presence of a header row for csv/tsv [default: ``true``] - % :type hdr: boolean + % :type hdr: logical % % % :returns: - :file_content: corresponding data array or structure @@ -23,8 +23,10 @@ % Based on spm_load.m from SPM12. % % + % (C) Copyright 2018 Guillaume Flandin, Wellcome Centre for Human Neuroimaging % + % (C) Copyright 2018 BIDS-MATLAB developers % -Check input arguments diff --git a/+bids/+util/tsvwrite.m b/+bids/+util/tsvwrite.m index 57124f8b..697a7745 100644 --- a/+bids/+util/tsvwrite.m +++ b/+bids/+util/tsvwrite.m @@ -15,8 +15,10 @@ function tsvwrite(filename, var) % Based on spm_save.m from SPM12. % % + % (C) Copyright 2018 Guillaume Flandin, Wellcome Centre for Human Neuroimaging % + % (C) Copyright 2018 BIDS-MATLAB developers delim = sprintf('\t'); @@ -80,7 +82,9 @@ function write_to_file(filename, var, delim) fid = fopen(filename, 'Wt'); if fid == -1 - error('Unble to write file %s.', filename); + error(['Unable to open file "%s" for writing.', ... + '\nIf you are using a datalad dataset, make sure the file is unlocked.'], ... + bids.internal.file_utils(filename, 'cpath')); end for i = 1:size(var, 1) diff --git a/+bids/Contents.m b/+bids/Contents.m index 4feccdfc..b8f5a669 100644 --- a/+bids/Contents.m +++ b/+bids/Contents.m @@ -1,40 +1,19 @@ -% +BIDS - The bids-matlab library +% +BIDS % -% Contents -% layout - Parse a directory structure formatted according to the BIDS standard -% query - Queries a directory structure formatted according to the BIDS standard -% validate - BIDS Validator -% report - Create a short summary of the acquisition parameters of a BIDS dataset -% copy_to_derivative - Copy selected data from BIDS layout to given derivatives folder, -% File - Class to handle BIDS filenames. +% Files +% bids_matlab_version - Return bids matlab version. +% copy_to_derivative - Copy selected data from BIDS layout to given derivatives folder. +% derivatives_json - Creates dummy content for a given BIDS derivative file. % Description - Class to deal with dataset_description files. +% diagnostic - Create figure listing the number of files for each subject +% File - Class to deal with BIDS files and to help to create BIDS valid names % init - Initialize dataset with README, description, folder structure... -% derivatives_json - Creates dummy content for a given BIDS derivative file. +% layout - Parse a directory structure formatted according to the BIDS standard +% Model - Class to deal with BIDS stats models +% query - Queries a directory structure formatted according to the BIDS standard +% report - Create a short summary of the acquisition parameters of a BIDS dataset. % Schema - Class to interact with the BIDS schema -% -% util.jsondecode - Decode JSON-formatted file -% util.jsonencode - Encode data to JSON-formatted file -% util.mkdir - Make new directory trees -% util.tsvread - Load text and numeric data from tab-separated-value or other file -% util.tsvwrite - Save text and numeric data to .tsv file -% -% -% __________________________________________________________________________ -% -% BIDS-MATLAB is a library that aims at centralising MATLAB/Octave tools -% for interacting with datasets conforming to the BIDS format. -% See https://github.com/bids-standard/bids-matlab for more details. -% -% __________________________________________________________________________ -% -% BIDS (Brain Imaging Data Structure): https://bids.neuroimaging.io/ -% -% The brain imaging data structure, a format for organizing and -% describing outputs of neuroimaging experiments. -% K. J. Gorgolewski et al, Scientific Data, 2016. -% __________________________________________________________________________ -% -% -% (C) Copyright 2016-2018 Guillaume Flandin, Wellcome Centre for Human Neuroimaging -% -% (C) Copyright 2018 BIDS-MATLAB developers +% transformers - Apply transformers to a structure +% validate - BIDS Validator + +% (C) Copyright 2022 BIDS-MATLAB developers diff --git a/+bids/Description.m b/+bids/Description.m index 33f5754d..0bcf9313 100644 --- a/+bids/Description.m +++ b/+bids/Description.m @@ -8,21 +8,23 @@ % % :param pipeline: pipeline name % :type pipeline: string + % % :param BIDS: output from BIDS layout to identify the source dataset % used when creating a derivatives dataset - % :type BIDS: structure + % Can also be the path to a dataset_descriptin.json file + % :type BIDS: structure or char % % + % (C) Copyright 2021 BIDS-MATLAB developers - % TODO - % - transfer validate function of layout in here + % TODO transfer validate function of layout in here properties content % dataset description content - is_derivative = false % boolean + is_derivative = false % logical pipeline = '' % name of the pipeline used to generate this derivative dataset @@ -32,24 +34,43 @@ methods - function obj = Description(pipeline, BIDS) + function obj = Description(varargin) + % + % USAGE:: % - % Instance constructor + % ds_desc = bids.Description(pipeline, BIDS); % - if nargin > 0 + args = inputParser; + + char_or_struct = @(x) ischar(x) || isstruct(x); + + addOptional(args, 'pipeline', '', @ischar); + addOptional(args, 'BIDS', '', char_or_struct); + + parse(args, varargin{:}); + + pipeline = args.Results.pipeline; + BIDS = args.Results.BIDS; + + %% + + if ~isempty(pipeline) obj.is_derivative = true; - if ~isempty(pipeline) - obj.pipeline = pipeline; - end + obj.pipeline = pipeline; end - if nargin > 1 && ~isempty(BIDS) - obj.source_description = BIDS.description; + if ~isempty(BIDS) + if isstruct(BIDS) + obj.source_description = BIDS.description; + elseif ischar(BIDS) && ... + exist(BIDS, 'file') && ... + strcmp(bids.internal.file_utils(BIDS, 'filename'), 'dataset_description.json') + obj.source_description = bids.util.jsondecode(BIDS); + end end - obj.content = struct( ... - 'Name', '', ... + obj.content = struct('Name', '', ... 'BIDSVersion', '', ... 'DatasetType', 'raw', ... 'License', '', ... diff --git a/+bids/File.m b/+bids/File.m index 8b59dbf0..3c141b04 100644 --- a/+bids/File.m +++ b/+bids/File.m @@ -1,6 +1,7 @@ classdef File % - % Class to deal with BIDS files and to help to create BIDS valid names + % Class to deal with BIDS filenames + % % % USAGE:: % @@ -9,14 +10,17 @@ % 'tolerant', true, % 'verbose', false); % - % :param input: - % :type input: filename or structure - % :param use_schema: - % :type use_schema: boolean - % :param tolerant: turns errors into warning - % :type tolerant: boolean - % :param verbose: silences warnings - % :type verbose: boolean + % :param input: path to the file or a structure with the file information + % :type input: filename or structure + % + % :param use_schema: will apply the BIDS schema when parsing or creating filenames + % :type use_schema: logical + % + % :param tolerant: turns errors into warning when set to ``true`` + % :type tolerant: logical + % + % :param verbose: silences warnings + % :type verbose: logical % % % **Initialize with a filename** @@ -62,6 +66,7 @@ % % file = bids.File(name_spec, 'use_schema', true); % + % (C) Copyright 2021 BIDS-MATLAB developers properties @@ -76,25 +81,32 @@ modality = '' % name of file modality + path = '' % absolute path + bids_path = '' % path within dataset filename = '' % bidsified name json_filename = '' % bidsified name for json file + metadata_files = {} % list of metadata files related + + metadata % list of metadata for this file + entity_required = {} % Required entities entity_order = {} % Expected order of entities schema = [] % BIDS schema used + tolerant = true + + verbose = false + end properties (SetAccess = private) changed = false - tolerant = true - verbose = false - end methods @@ -113,17 +125,21 @@ obj.tolerant = args.Results.tolerant; obj.verbose = args.Results.verbose; + if args.Results.use_schema + obj.schema = bids.Schema(); + end + if isempty(args.Results.input) f_struct = struct([]); elseif ischar(args.Results.input) + if ~isempty(fileparts(args.Results.input)) + obj.path = args.Results.input; + end f_struct = bids.internal.parse_filename(args.Results.input); elseif isstruct(args.Results.input) f_struct = args.Results.input; end - obj.verbose = args.Results.verbose; - obj.tolerant = args.Results.tolerant; - if isfield(f_struct, 'prefix') obj.prefix = f_struct.prefix; end @@ -142,10 +158,16 @@ obj.entities = f_struct.entities; end + if isfield(f_struct, 'modality') + obj.modality = f_struct.modality; + end + if args.Results.use_schema obj = obj.use_schema(); end + obj = obj.set_metadata(); + obj = obj.update(); end @@ -215,7 +237,7 @@ for ifn = 1:size(fn, 1) key = fn{ifn}; obj.validate_word(key, 'Entity label'); - val = entities.(key); + val = bids.internal.camel_case(entities.(key)); if isempty(val) continue end @@ -223,7 +245,7 @@ obj.validate_word(val, 'Entity value'); end - if ~contain_value + if ~strcmp(obj.filename, 'participants.tsv') && ~contain_value obj.bids_file_error('noEntity', 'No entity-label pairs'); end @@ -237,14 +259,29 @@ obj.changed = true; end + function obj = set_metadata(obj) + if isempty(obj.metadata_files) + pattern = '^.*%s\\.json$'; + obj.metadata_files = bids.internal.get_meta_list(obj.path, pattern); + end + obj.metadata = bids.internal.get_metadata(obj.metadata_files); + end + function obj = set_entity(obj, label, value) obj.validate_word(label, 'Entity label'); obj.validate_word(value, 'Entity value'); - obj.entities(1).(label) = value; + obj.entities(1).(label) = bids.internal.camel_case(value); obj.changed = true; end + function obj = set_metadata_files(obj, pattern) + if nargin < 2 + pattern = '^.*%s\\.json$'; + end + obj.metadata_files = bids.internal.get_meta_list(obj.path, pattern); + end + %% other methods function obj = update(obj) % @@ -252,13 +289,13 @@ % fname = ''; - path = ''; + path = ''; %#ok<*PROP> fn = fieldnames(obj.entities); for i = 1:size(fn, 1) key = fn{i}; - val = obj.entities.(key); + val = bids.internal.camel_case(obj.entities.(key)); if isempty(val) continue end @@ -309,7 +346,10 @@ % % USAGE:: % - % file = file.reorder_entities([entity_order]); + % file = file.reorder_entities(entity_order); + % + % :param entity_order: Optional. The order of the entities. + % :type entity_order: cell of char % % If the no entity order is provided, it will try to rely on the schema to % find an appropriate order @@ -333,9 +373,17 @@ if nargin > 1 && ~isempty(entity_order) order = entity_order; - elseif ~isempty(obj.schema) - obj = get_entity_order_from_schema(obj); - order = obj.entity_order; + else + if ~isempty(obj.schema) + obj = get_entity_order_from_schema(obj); + order = obj.entity_order; + else + schema = bids.Schema; + entities = schema.entity_order(); + for i = 1:numel(entities) + order{i, 1} = schema.return_entity_key(entities{i}); + end + end end if size(order, 2) > 1 @@ -343,7 +391,17 @@ end entity_names = fieldnames(obj.entities); idx = ismember(entity_names, order); + obj.entity_order = cat(1, order, entity_names(~idx)); + % forget about extra entities when using the schema + if ~isempty(obj.schema) + obj.entity_order = order; + if any(~idx) + obj.bids_file_error('extraEntityForSuffix', ... + sprintf('Unknown entity for suffix "%s": "%s"', ... + obj.suffix, strjoin(entity_names(~idx), ', '))); + end + end % reorder obj.entities tmp = struct(); @@ -358,6 +416,132 @@ end + function obj = rename(obj, varargin) + % + % Renames a file according following some specification + % + % USAGE:: + % + % file = file.rename('spec', spec, 'dry_run', true, 'verbose', [], 'force', false); + % + % :param spec: structure specifying what entities, suffix, extension... to apply + % :type spec: structure + % + % :param dry_run: If ``true`` no file is actually renamed. + % ``true`` is the default to avoid renaming files by mistake. + % :type dry_run: logical + % + % :param verbose: displays ``input --> output`` + % :type verbose: logical + % + % :param force: Overwrites existing file. + % :type force: logical + % + % EXAMPLE: + % + % .. code-block:: matlab + % + % %% rename an SPM preprocessed file + % + % % expected_name = fullfile(pwd, ... + % % 'sub-01', ... + % % 'sub-01_task-faceRep_space-individual_desc-preproc_bold.nii'); + % + % input_filename = 'uasub-01_task-faceRep_bold.nii'; + % + % file = bids.File(input_filename, 'use_schema', false); + % + % spec.prefix = ''; % remove prefix + % spec.entities.desc = 'preproc'; % add description entity + % spec.entity_order = {'sub', 'task', 'desc'}; + % + % file = file.rename('spec', spec, 'dry_run', false, 'verbose', true); + % + % + % %% Get a specific file from a dataset to rename + % + % BIDS = bids.layout(path_to_dataset) + % + % % construct a filter to get only the file we want/ + % subject = '001'; + % run = '001'; + % suffix = 'bold'; + % task = 'faceRep'; + % filter = struct('sub', subject, 'task', task, 'run', run, 'suffix', suffix); + % + % file_to_rename = bids.query(BIDS, 'data', filter); + % + % file = bids.File(file_to_rename, 'use_schema', false); + % + % % specification to remove run entity + % spec.entities.run = ''; + % + % % first run with dry_run = true to make sure we will get the expected output + % file = file.rename('spec', spec, 'dry_run', true, 'verbose', true); + % + % % rename the file by setting dry_run to false + % file = file.rename('spec', spec, 'dry_run', false, 'verbose', true); + % + + args = inputParser; + args.addParameter('dry_run', true, @islogical); + args.addParameter('force', false, @islogical); + args.addParameter('verbose', []); + args.addParameter('spec', struct([]), @isstruct); + args.parse(varargin{:}); + + if ~isempty(args.Results.spec) + spec = args.Results.spec; + if isfield(spec, 'prefix') + obj.prefix = spec.prefix; + end + if isfield(spec, 'suffix') + obj.suffix = spec.suffix; + end + if isfield(spec, 'ext') + obj.extension = spec.ext; + end + if isfield(spec, 'entities') + entities = fieldnames(spec.entities); %#ok<*PROPLC> + for i = 1:numel(entities) + obj = obj.set_entity(entities{i}, ... + bids.internal.camel_case(spec.entities.(entities{i}))); + end + end + if isfield(spec, 'entity_order') + obj = obj.reorder_entities(spec.entity_order); + end + + obj = obj.update; + end + + if ~isempty(args.Results.verbose) && islogical(args.Results.verbose) + obj.verbose = args.Results.verbose; + end + + if obj.verbose + fprintf(1, '%s --> %s\n', bids.internal.format_path(obj.path), ... + bids.internal.format_path(fullfile(fileparts(obj.path), obj.filename))); + end + + if ~args.Results.dry_run + % TODO update obj.path + output_file = fullfile(fileparts(obj.path), obj.filename); + if ~exist(output_file, 'file') || args.Results.force + movefile(obj.path, output_file); + obj.path = output_file; + else + bids.internal.error_handling(mfilename(), 'fileAlreadyExists', ... + sprintf(['file %s already exist. ', ... + 'Use ''force'' to overwrite.'], ... + bids.internal.format_path(output_file)), ... + obj.tolerant, ... + obj.verbose); + end + end + + end + %% schema related methods function obj = use_schema(obj) % @@ -373,7 +557,10 @@ % file = file.use_schema(); % - obj.schema = bids.Schema(); + if isempty(obj.schema) + obj.schema = bids.Schema(); + end + obj = obj.get_required_entities(); obj = obj.get_entity_order_from_schema(); obj.validate_entities(); @@ -399,7 +586,7 @@ function validate_entities(obj) present_entities = fieldnames(obj.entities); forbidden_entity = ~ismember(present_entities, obj.entity_order); if any(forbidden_entity) - msg = sprintf(['Entitiy ''%s'' not allowed by BIDS schema.', ... + msg = sprintf(['Entitiy "%s" not allowed by BIDS schema.', ... '\nAllowed entities are:\n - %s'], ... present_entities{forbidden_entity}, ... strjoin(obj.entity_order, '\n - ')); @@ -419,7 +606,9 @@ function validate_entities(obj) obj.bids_file_error('schemaMissing'); end - obj = obj.get_modality_from_schema(); + if isempty(obj.modality) + obj = obj.get_modality_from_schema(); + end if isempty(obj.modality) || iscell(obj.modality) return end @@ -471,17 +660,27 @@ function validate_entities(obj) obj.bids_file_error('schemaMissing'); end - obj = obj.get_modality_from_schema(); + if isempty(obj.modality) + obj = obj.get_modality_from_schema(); + end if isempty(obj.modality) || iscell(obj.modality) return end schema_entities = obj.schema.return_entities_for_suffix_modality(obj.suffix, ... obj.modality); - for i = 1:numel(schema_entities) - obj.entity_order{i, 1} = schema_entities{i}; + % reorder entities because they are not always ordered in the schema + % TODO make faster + entity_order = {}; + for i = 1:numel(obj.schema.entity_order) + this_entity = obj.schema.entity_order{i}; + short_name = obj.schema.content.objects.entities.(this_entity).name; + if ismember(short_name, schema_entities) + entity_order{end + 1, 1} = short_name; + end end - entity_order = obj.entity_order; + + obj.entity_order = entity_order; end @@ -514,10 +713,13 @@ function bids_file_error(obj, id, msg) msg = ''; end + if isempty(obj.schema) && ismember(id, {'prefixDefined'}) + return + end + switch id case 'noEntity' msg = 'No entity-label pairs.'; - obj.tolerant = false; case 'schemaMissing' msg = 'no schema specified: run file.use_schema()'; diff --git a/+bids/Model.m b/+bids/Model.m new file mode 100644 index 00000000..a4762765 --- /dev/null +++ b/+bids/Model.m @@ -0,0 +1,1011 @@ +classdef Model + % + % Class to deal with BIDS stats models + % + % See the `BIDS stats model website + % `_ + % for more information. + % + % USAGE:: + % + % bm = bids.Model('init', true, ... + % 'file', path_to_bids_stats_model_file, ... + % 'tolerant', true, + % 'verbose', false); + % + % :param init: if ``true`` this will initialize an empty model. Defaults to ``false``. + % :type init: logical + % + % :param file: fullpath the JSON file containing the BIDS stats model + % :type file: path + % + % :param tolerant: turns errors into warning + % :type tolerant: logical + % + % :param verbose: silences warnings + % :type verbose: logical + % + % EXAMPLE:: + % + % % initialize and write an empty model + % bm = bids.Model('init', true); + % filename = fullfile(pwd, 'model-foo_smdl.json'); + % bm.write(filename); + % + % EXAMPLE:: + % + % % load a stats model from a file + % model_file = fullfile(get_test_data_dir(), ... + % '..', ... + % 'data', ... + % 'model', ['model-narps_smdl.json']); + % + % bm = bids.Model('file', model_file, 'verbose', false); + % + % + + % (C) Copyright 2022 Remi Gau + + properties + + content = '' % "raw" content of a loaded JSON + + Name = 'REQUIRED' % Name of the model + + Description = 'RECOMMENDED' % Description of the model + + BIDSModelVersion = '1.0.0' % Version of the model + + Input = 'REQUIRED' % Input of the model + + Nodes = {'REQUIRED'} % Nodes of the model + + Edges = {} % Edges of the model + + tolerant = true % if ``true`` turns error into warning + + verbose = true % hides warning if ``false`` + + end + + methods + + function obj = Model(varargin) + + args = inputParser; + + is_file = @(x) exist(x, 'file'); + + args.addParameter('init', false, @islogical); + args.addParameter('file', '', is_file); + args.addParameter('tolerant', obj.tolerant, @islogical); + args.addParameter('verbose', obj.verbose, @islogical); + + args.parse(varargin{:}); + + obj.tolerant = args.Results.tolerant; + obj.verbose = args.Results.verbose; + + if args.Results.init || strcmp(args.Results.file, '') + + obj.Name = 'empty_model'; + obj.Description = 'This is an empty BIDS stats model.'; + obj.Input = struct('task', {{''}}); + obj.Nodes{1} = bids.Model.empty_node('run'); + + obj = update(obj); + + return + end + + if ~isempty(args.Results.file) + + obj.content = bids.util.jsondecode(args.Results.file); + + if ~isfield(obj.content, 'Name') + bids.internal.error_handling(mfilename(), ... + 'nameRequired', ... + 'Name field required', ... + obj.tolerant, ... + obj.verbose); + else + obj.Name = obj.content.Name; + end + + if isfield(obj.content, 'Description') + obj.Description = obj.content.Description; + end + + if ~isfield(obj.content, 'BIDSModelVersion') + bids.internal.error_handling(mfilename(), ... + 'BIDSModelVersionRequired', ... + 'BIDSModelVersion field required', ... + obj.tolerant, ... + obj.verbose); + else + obj.BIDSModelVersion = obj.content.BIDSModelVersion; + end + + if ~isfield(obj.content, 'Input') + bids.internal.error_handling(mfilename(), ... + 'InputRequired', ... + 'Input field required', ... + obj.tolerant, ... + obj.verbose); + else + obj.Input = obj.content.Input; + end + + % Nodes are coerced into cells + % to make easier to deal with them later + if ~isfield(obj.content, 'Nodes') + bids.internal.error_handling(mfilename(), ... + 'NodesRequired', ... + 'Nodes field required', ... + obj.tolerant, ... + obj.verbose); + else + + if iscell(obj.content.Nodes) + obj.Nodes = obj.content.Nodes; + elseif isstruct(obj.content.Nodes) + for iNode = 1:numel(obj.content.Nodes) + obj.Nodes{iNode, 1} = obj.content.Nodes(iNode); + end + end + + end + + % Contrasts are coerced into cells + % to make easier to deal with them later + for iNode = 1:numel(obj.content.Nodes) + if isfield(obj.Nodes{iNode, 1}, 'Contrasts') && isstruct(obj.Nodes{iNode, 1}.Contrasts) + for iCon = 1:numel(obj.Nodes{iNode, 1}.Contrasts) + tmp{iCon, 1} = obj.Nodes{iNode, 1}.Contrasts(iCon); + end + obj.Nodes{iNode, 1}.Contrasts = tmp; + clear tmp; + end + end + + % Edges are coerced into cells + % to make easier to deal with them later + if isfield(obj.content, 'Edges') + if iscell(obj.content.Edges) + obj.Edges = obj.content.Edges; + elseif isstruct(obj.content.Edges) + for i = 1:numel(obj.content.Edges) + obj.Edges{i, 1} = obj.content.Edges(i); + end + end + + else + obj = get_edges_from_nodes(obj); + + end + + obj.validate(); + + end + + end + + %% Setters + function obj = set.Name(obj, name) + obj.Name = name; + end + + function obj = set.Description(obj, desc) + obj.Description = desc; + end + + function obj = set.BIDSModelVersion(obj, version) + obj.BIDSModelVersion = version; + end + + function obj = set.Input(obj, input) + obj.Input = input; + end + + function obj = set.Nodes(obj, nodes) + obj.Nodes = nodes; + end + + function obj = set.Edges(obj, edges) + if nargin < 2 + edges = []; + end + if isempty(edges) + % assume nodes follow each other linearly + obj = get_edges_from_nodes(obj); + else + obj.Edges = edges; + end + end + + %% Getters + function value = get.Name(obj) + value = obj.Name; + end + + function value = get.Input(obj) + value = obj.Input; + end + + function value = get.Nodes(obj) + value = obj.Nodes; + end + + function value = get.Edges(obj) + value = obj.Edges; + end + + function [value, idx] = get_nodes(obj, varargin) + % + % Get a specific node from the model given its Level and / or Name. + % + % USAGE:: + % + % [value, idx] = bm.get_nodes('Level', '', 'Name', '') + % + % + % :param Level: Must be one of ``Run``, ``Session``, ``Subject``, ``Dataset``. + % Default to ``''`` + % :type init: string + % + % :param Name: Default to ``''`` + % :type file: path + % + % + % Returns: value - Node(s) as struct if there is only one or a cell if more + % idx - Node index + % + % + % EXAMPLE:: + % + % bm = bids.Model('file', model_file('narps'), 'verbose', false); + % + % % Get all nodes + % bm.get_nodes() + % + % % Get run level node + % bm.get_nodes('Level', 'Run') + % + % % Get the "Negative" node + % bm.get_nodes('Name', 'negative') + % + % + if isempty(varargin) + value = obj.Nodes; + idx = 1:numel(value); + + value = format_output(value, idx); + + return + + end + + value = {}; + + allowed_levels = @(x) all(ismember(lower(x), {'', 'run', 'session', 'subject', 'dataset'})); + + args = inputParser; + args.addParameter('Level', '', allowed_levels); + args.addParameter('Name', ''); + args.parse(varargin{:}); + + Level = lower(args.Results.Level); + Name = lower(args.Results.Name); %#ok<*PROPLC> + + % return all nodes if no argument is given + if strcmp(Name, '') && strcmp(Level, '') + value = obj.Nodes; + idx = 1:numel(value); + + value = format_output(value, idx); + + return + + end + + % otherwise we identify them by the arguments given + % Name takes precedence as Name are supposed to be unique + if ~strcmp(Level, '') + if ischar(Level) + Level = {Level}; + end + levels = cellfun(@(x) lower(x.Level), obj.Nodes, 'UniformOutput', false); + idx = ismember(levels, Level); + end + + if ~strcmp(Name, '') + if ischar(Name) + Name = {Name}; + end + names = cellfun(@(x) lower(x.Name), obj.Nodes, 'UniformOutput', false); + idx = ismember(names, Name); + end + + % TODO merge idx when both Level and Name are passed as parameters ? + if any(idx) + idx = find(idx); + for i = 1:numel(idx) + value{end + 1} = obj.Nodes{idx(i)}; + end + + else + for i = 1:numel(obj.Nodes) + tmp{i} = ['Name: "', obj.Nodes{i}.Name '"; ', ... + 'Level: "' obj.Nodes{i}.Level '"']; %#ok + end + msg = sprintf(['Could not find a corresponding Node with', ... + '\n Name: "%s"; Level: "%s"', ... + '\n\n Available nodes:%s'], ... + char(Name), char(Level), ... + bids.internal.create_unordered_list(tmp)); + + bids.internal.error_handling(mfilename(), 'missingNode', msg, ... + obj.tolerant, ... + obj.verbose); + end + + value = format_output(value, idx); + + % local subfunction to ensure that cells are returned if more than one + % node and struct otherwise + function value = format_output(value, idx) + if ~iscell(value) && numel(idx) > 1 + value = {value}; + elseif iscell(value) && numel(value) == 1 + value = value{1}; + end + end + + end + + function source_nodes = get_source_node(obj, node_name) + + source_nodes = {}; + + if isempty(obj.Edges) + obj = obj.get_edges_from_nodes; + end + + if strcmp(node_name, obj.Edges{1}.Source) + % The root node cannot have a source + return + end + + % we should only get 1 value + for i = 1:numel(obj.Edges) + if strcmp(obj.Edges{i}.Destination, node_name) + source = obj.Edges{i}.Source; + source_nodes{end + 1} = obj.get_nodes('Name', source); + end + end + + assert(numel(source_nodes) == 1); + + if numel(source_nodes) == 1 + source_nodes = source_nodes{1}; + end + + end + + function [root_node, root_node_name] = get_root_node(obj) + + edges = obj.Edges; + + if isempty(edges) + root_node = obj.Nodes(1); + + elseif iscell(edges) + root_node_name = edges{1}.Source; + root_node = obj.get_nodes('Name', root_node_name); + + elseif isstruct(edges(1)) + root_node_name = edges(1).Source; + root_node = obj.get_nodes('Name', root_node_name); + + else + root_node = obj.Nodes(1); + + end + + if iscell(root_node) + root_node = root_node{1}; + end + + root_node_name = root_node.Name; + + end + + function edge = get_edge(obj, field, value) + % + % USAGE:: + % + % edge = bm.get_edges(field, value) + % + % + % field can be any of {'Source', 'Destination'} + % + + edge = {}; + + if ~ismember(field, {'Source', 'Destination'}) + bids.internal.error_handling(mfilename(), ... + 'wrongEdgeQuery', ... + 'Can only query Edges based on Source or Destination', ... + obj.tolerant, ... + obj.verbose); + end + + if isempty(obj.Edges) + obj = obj.get_edges_from_nodes; + end + + % for 'Destination' we should only get a single value + % for 'Source' we can get several + for i = 1:numel(obj.Edges) + if strcmp(obj.Edges{i}.(field), value) + edge{end + 1} = obj.Edges{i}; + end + end + + if isempty(edge) + msg = sprintf('Could not find a corresponding Edge.'); + bids.internal.error_handling(mfilename(), 'missingEdge', msg, ... + obj.tolerant, ... + obj.verbose); + end + + if strcmp(field, 'Destination') && numel(edge) > 1 + msg = sprintf('Getting more than one Edge with Destination %s.', value); + bids.internal.error_handling(mfilename(), 'tooManyEdges', msg, ... + obj.tolerant, ... + obj.verbose); + end + + if numel(edge) == 1 + edge = edge{1}; + end + + end + + function obj = get_edges_from_nodes(obj) + % + % Generates all the default edges from the list of nodes in the model. + % + % USAGE:: + % + % bm = bm.get_edges_from_nodes(); + % edges = bm.Edges(); + % + if numel(obj.Nodes) <= 1 + return + end + for i = 1:(numel(obj.Nodes) - 1) + obj.Edges{i, 1} = struct('Source', obj.Nodes{i, 1}.Name, ... + 'Destination', obj.Nodes{i + 1, 1}.Name); + end + end + + function value = node_names(obj) + value = cellfun(@(x) x.Name, obj.Nodes, 'UniformOutput', false); + end + + function validate(obj) + % + % Very light validation of fields that were not checked on loading. + % Automatically run on loaoding of a dataset. + % + % USAGE:: + % + % bm.validate() + % + + REQUIRED_NODES_FIELDS = {'Level', 'Name', 'Model'}; + REQUIRED_TRANSFORMATIONS_FIELDS = {'Transformer', 'Instructions'}; + REQUIRED_MODEL_FIELDS = {'Type', 'X'}; + REQUIRED_HRF_FIELDS = {'Variables', 'Model'}; + REQUIRED_CONTRASTS_FIELDS = {'Name', 'ConditionList', 'Weights', 'Test'}; + REQUIRED_DUMMY_CONTRASTS_FIELDS = {'Contrasts'}; + + % Nodes + nodes = obj.Nodes; + for i = 1:(numel(nodes)) + + this_node = nodes{i, 1}; + + fields_present = fieldnames(this_node); + if any(~ismember(REQUIRED_NODES_FIELDS, fields_present)) + obj.model_validation_error('Nodes', REQUIRED_NODES_FIELDS); + end + + check = struct('Model', {REQUIRED_MODEL_FIELDS}, ... + 'Transformations', {REQUIRED_TRANSFORMATIONS_FIELDS}, ... + 'DummyConstrasts', {REQUIRED_DUMMY_CONTRASTS_FIELDS}, ... + 'Contrasts', {REQUIRED_CONTRASTS_FIELDS}); + + field_to_check = fieldnames(check); + + for j = 1:numel(field_to_check) + + if ~isfield(this_node, field_to_check{j}) + continue + end + + if strcmp(field_to_check{j}, 'Contrasts') + + for contrast = 1:numel(this_node.Contrasts) + + fields_present = bids.Model.get_keys(this_node.Contrasts(contrast)); + if ~iscellstr(fields_present) + fields_present = fields_present{1}; + end + + if any(~ismember(check.Contrasts, fields_present)) + obj.model_validation_error('Contrasts', check.Contrasts); + end + + obj.validate_constrasts(this_node); + + end + + else + + fields_present = bids.Model.get_keys(this_node.(field_to_check{j})); + + if any(~ismember(check.(field_to_check{j}), fields_present)) + obj.model_validation_error(field_to_check{j}, check.(field_to_check{j})); + end + + end + + if strcmp(field_to_check{j}, 'Model') + + if isfield(this_node.Model, 'HRF') && ~isempty(this_node.Model.HRF) + + fields_present = fieldnames(this_node.Model.HRF); + if any(~ismember(REQUIRED_HRF_FIELDS, fields_present)) + obj.model_validation_error('HRF', REQUIRED_HRF_FIELDS); + end + + end + + end + + end + + end + + if numel(nodes) > 1 + obj.validate_edges(); + end + + end + + function validate_edges(obj) + % + % USAGE:: + % + % bm.validate_edges() + % + + REQUIRED_EDGES_FIELDS = {'Source', 'Destination'}; + + edges = obj.Edges; + + if ~isempty(edges) + + all_nodes = {}; + + for i = 1:(numel(edges)) + + this_edge = edges{i, 1}; + + all_nodes{end + 1} = this_edge.Source; + all_nodes{end + 1} = this_edge.Destination; + + if ~isstruct(this_edge) + obj.model_validation_error('Edges', REQUIRED_EDGES_FIELDS); + continue + + end + + fields_present = fieldnames(this_edge); + if any(~ismember(REQUIRED_EDGES_FIELDS, fields_present)) + obj.model_validation_error('Edges', REQUIRED_EDGES_FIELDS); + end + + if ~ismember(this_edge.Source, obj.node_names()) || ... + ~ismember(this_edge.Destination, obj.node_names()) + + bids.internal.error_handling(mfilename(), ... + 'edgeRefersToUnknownNode', ... + sprintf(['Edge refers to unknown Node. ', ... + 'Available Nodes: %s.'], ... + strjoin(obj.node_names(), ', ')), ... + obj.tolerant, ... + obj.verbose); + + end + + end + + all_nodes = unique(all_nodes); + node_names = obj.node_names(); + missing_nodes = ~ismember(obj.node_names(), all_nodes); + if any(missing_nodes) + bids.internal.error_handling(mfilename(), ... + 'nodeMissingFromEdges', ... + sprintf(['\nNodes named "%s" missing from "Edges":', ... + 'they will not be run.'], ... + strjoin(node_names(missing_nodes), ', ')), ... + obj.tolerant, ... + obj.verbose); + end + + end + + end + + %% Node level methods + % assumes that only one node is being queried + function [value, idx] = get_transformations(obj, varargin) + % + % USAGE:: + % + % transformations = bm.get_transformations('Name', 'node_name') + % + % + % :param Name: name of the node whose transformations we want + % :type Name: char + % + value = []; + [node, idx] = get_nodes(obj, varargin{:}); + assert(numel(node) == 1); + if isfield(node, 'Transformations') + value = node.Transformations; + end + end + + function [value, idx] = get_dummy_contrasts(obj, varargin) + % + % USAGE:: + % + % dummy_contrasts = bm.get_dummy_contrasts('Name', 'node_name') + % + % :param Name: name of the node whose dummy contrasts we want + % :type Name: char + % + value = []; + [node, idx] = get_nodes(obj, varargin{:}); + assert(numel(node) == 1); + if isfield(node, 'DummyContrasts') + value = node.DummyContrasts; + end + end + + function [value, idx] = get_contrasts(obj, varargin) + % + % USAGE:: + % + % contrasts = bm.get_contrasts('Name', 'node_name') + % + % :param Name: name of the node whose contrasts we want + % :type Name: char + % + value = []; + [node, idx] = get_nodes(obj, varargin{:}); + assert(numel(node) == 1); + if isfield(node, 'Contrasts') + value = node.Contrasts; + end + end + + function [value, idx] = get_model(obj, varargin) + % + % USAGE:: + % + % model = bm.get_model('Name', 'node_name') + % + % :param Name: name of the node whose model we want + % :type Name: char + % + [node, idx] = get_nodes(obj, varargin{:}); + assert(numel(node) == 1); + value = node.Model; + end + + function value = get_design_matrix(obj, varargin) + % + % USAGE:: + % + % matrix = bm.get_design_matrix('Name', 'node_name') + % + % :param Name: name of the node whose model matrix we want + % :type Name: char + % + model = get_model(obj, varargin{:}); + value = model.X; + end + + %% Other + function obj = default(obj, varargin) + % + % Generates a default BIDS stats model for a given data set + % + % USAGE:: + % + % bm = bm.default(BIDS, tasks) + % + % :param BIDS: fullpath to a BIDS dataset or output structure from ``bids.layout`` + % :type BIDS: path or structure + % + % :param tasks: tasks to include in the model + % :type tasks: char or cellstr + % + % EXAMPLE:: + % + % pth_bids_example = get_test_data_dir(); + % BIDS = bids.layout(fullfile(pth_bids_example, 'ds003')); + % bm = bids.Model(); + % bm = bm.default(BIDS, 'rhymejudgement'); + % filename = fullfile(pwd, 'model-rhymejudgement_smdl.json'); + % bm.write(filename); + % + + is_dir_or_struct = @(x) isstruct(x) || isdir(x); %#ok<*ISDIR> + is_char_or_cellstr = @(x) ischar(x) || iscellstr(x); %#ok<*ISCLSTR> + + args = inputParser; + args.addRequired('layout', is_dir_or_struct); + args.addOptional('tasks', '', is_char_or_cellstr); + + args.parse(varargin{:}); + + tasks = args.Results.tasks; + if ischar(tasks) + tasks = cellstr(tasks); + end + if strcmp(tasks{1}, '') + tasks = bids.query(args.Results.layout, 'tasks'); + end + if isempty(tasks) + msg = sprintf('No task found in dataset %s', ... + bids.internal.format_path(args.Results.layout.pth)); + bids.internal.error_handling(mfilename(), ... + 'noTaskDetected', ... + msg, ... + obj.tolerant, ... + obj.verbose); + end + sessions = bids.query(args.Results.layout, 'sessions'); + + GroupBy_level_1 = {'run', 'subject'}; + if ~isempty(sessions) + GroupBy_level_1 = {'run', 'session', 'subject'}; + end + + obj.Input.task = tasks; + obj.Name = sprintf('default_%s_model', strjoin(tasks, '_')); + obj.Description = sprintf('default BIDS stats model for %s task', strjoin(tasks, '/')); + + % Define design matrix by including all trial_types and a constant + trial_type_list = bids.internal.list_all_trial_types(args.Results.layout, tasks, ... + 'verbose', obj.verbose, ... + 'tolerant', obj.tolerant); + trial_type_list = cellfun(@(x) strjoin({'trial_type.', x}, ''), ... + trial_type_list, ... + 'UniformOutput', false); + obj.Nodes{1}.Model.X = cat(1, trial_type_list, '1'); + + obj.Nodes{1}.GroupBy = GroupBy_level_1; + obj.Nodes{1}.Model.HRF.Variables = trial_type_list; + obj.Nodes{1}.DummyContrasts.Contrasts = trial_type_list; + + sessions = bids.query(args.Results.layout, 'sessions', 'task', tasks); + if ~isempty(sessions) + obj.Nodes{end + 1, 1} = bids.Model.empty_node('session'); + end + obj.Nodes{end + 1, 1} = bids.Model.empty_node('subject'); + obj.Nodes{end, 1} = rmfield(obj.Nodes{end, 1}, 'Transformations'); + obj.Nodes{end, 1}.Model = rmfield(obj.Nodes{end, 1}.Model, 'HRF'); + obj.Nodes{end + 1, 1} = bids.Model.empty_node('dataset'); + obj.Nodes{end, 1} = rmfield(obj.Nodes{end, 1}, 'Transformations'); + obj.Nodes{end, 1}.Model = rmfield(obj.Nodes{end, 1}.Model, 'HRF'); + + obj = get_edges_from_nodes(obj); + obj.validate(); + obj = obj.update(); + + end + + function obj = update(obj) + % + % Update ``content`` for writing + % + % USAGE:: + % + % bm = bm.update() + % + + obj.content.Name = obj.Name; + obj.content.BIDSModelVersion = obj.BIDSModelVersion; + obj.content.Description = obj.Description; + obj.content.Input = obj.Input; + + % coerce some fields of Nodes to make sure the output JSON is valid + obj.content.Nodes = obj.Nodes; + + for i = 1:numel(obj.content.Nodes) + + this_node = obj.content.Nodes{i}; + + if isnumeric(this_node.Model.X) && numel(this_node.Model.X) == 1 + this_node.Model.X = {this_node.Model.X}; + end + + if isfield(this_node, 'Contrasts') + for j = 1:numel(this_node.Contrasts) + + this_contrast = this_node.Contrasts{j}; + + if ~isempty(this_contrast.Weights) && ... + ~iscell(this_contrast.Weights) && ... + numel(this_contrast.Weights) == 1 + this_contrast.Weights = {this_contrast.Weights}; + end + + if isnumeric(this_contrast.ConditionList) && ... + numel(this_contrast.ConditionList) == 1 + this_contrast.ConditionList = {this_contrast.ConditionList}; + end + + this_node.Contrasts{j} = this_contrast; + + end + end + + obj.content.Nodes{i} = this_node; + + end + + obj.content.Edges = obj.Edges; + + end + + function write(obj, filename) + % + % USAGE:: + % + % bm.write(filename) + % + + obj = update(obj); + bids.util.mkdir(fileparts(filename)); + bids.util.jsonencode(filename, obj.content); + + end + + end + + methods (Static) + + function node = empty_node(level) + % + % USAGE:: + % + % node = Model.empty_node('run') + % + + node = struct('Level', [upper(level(1)) level(2:end)], ... + 'Name', level, ... + 'GroupBy', {{''}}, ... + 'Transformations', {bids.Model.empty_transformations()}, ... + 'Model', bids.Model.empty_model(), ... + 'Contrasts', {{bids.Model.empty_contrast()}}, ... + 'DummyContrasts', struct('Test', 't', ... + 'Contrasts', {{''}})); + + end + + function contrast = empty_contrast() + contrast = struct('Name', '', ... + 'ConditionList', {{''}}, ... + 'Weights', {{''}}, ... + 'Test', 't'); + end + + function transformations = empty_transformations() + % + % USAGE:: + % + % transformations = Model.empty_transformations() + % + + transformations = struct('Transformer', '', ... + 'Instructions', {{ + struct('Name', '', ... + 'Inputs', {{''}}) + }}); + + end + + function model = empty_model() + % + % USAGE:: + % + % model = Model.empty_model() + % + + model = struct('X', {{''}}, ... + 'Type', 'glm', ... + 'HRF', struct('Variables', {{''}}, ... + 'Model', 'DoubleGamma'), ... + 'Options', struct('HighPassFilterCutoffHz', 0.008, ... + 'Mask', struct('desc', {{'brain'}}, ... + 'suffix', {{'mask'}})), ... + 'Software', {{}}); + + end + + function values = get_keys(cell_or_struct) + if iscell(cell_or_struct) + values = cellfun(@(x) fieldnames(x), cell_or_struct, 'UniformOutput', false); + elseif isstruct(cell_or_struct) + values = fieldnames(cell_or_struct); + end + end + + % could be made static + function validate_constrasts(node) + + if ~isfield(node, 'Contrasts') + return + end + + for iCon = 1:numel(node.Contrasts) + + if ~isfield(node.Contrasts{iCon}, 'Weights') + msg = sprintf('No weights specified for Contrast %s of Node %s', ... + node.Contrasts{iCon}.Name, node.Name); + bids.internal.error_handling(mfilename(), ... + 'weightsRequired', ... + msg, ... + obj.tolerant, ... + obj.verbose); + end + + if numel(node.Contrasts{iCon}.Weights) ~= numel(node.Contrasts{iCon}.ConditionList) + msg = sprintf('Number of Weights and Conditions unequal for Contrast %s of Node %s', ... + node.Contrasts{iCon}.Name, node.Name); + bids.internal.error_handling(mfilename(), ... + 'numelWeightsConditionMismatch', ... + msg, ... + obj.tolerant, ... + obj.verbose); + end + + end + + end + + end + + methods (Access = protected) + + function model_validation_error(obj, key, required_fields) + bids.internal.error_handling(mfilename(), ... + 'missingField', ... + sprintf('%s require the fields: %s.', ... + key, ... + strjoin(required_fields, ', ')), ... + obj.tolerant, ... + obj.verbose); + end + + end + +end diff --git a/+bids/Schema.m b/+bids/Schema.m index 3ebe8f3e..c05e0e28 100644 --- a/+bids/Schema.m +++ b/+bids/Schema.m @@ -6,9 +6,10 @@ % % schema = bids.Schema(use_schema) % - % use_schema: boolean + % use_schema: logical % % + % (C) Copyright 2021 BIDS-MATLAB developers % TODO use schema to access regular expressions @@ -32,7 +33,7 @@ % % schema = bids.Schema(use_schema) % - % use_schema: boolean + % use_schema: logical % obj.content = []; @@ -58,7 +59,8 @@ schema_file = fullfile(bids.internal.root_dir(), 'schema.json'); if ~exist(schema_file, 'file') - msg = sprintf('The schema.json file %s does not exist.', schema_dir); + msg = sprintf('The schema.json file %s does not exist.', ... + bids.internal.format_path(schema_file)); bids.internal.error_handling(mfilename(), 'missingSchema', msg, false, true); end @@ -75,7 +77,6 @@ for i = 1:numel(mod_grps) mods = obj.return_modalities([], mod_grps{i}); - for j = 1:numel(mods) suffix_groups = obj.list_suffix_groups(mods{j}, 'raw'); @@ -87,6 +88,10 @@ end + function sts = eq(obj) + sts = true; + end + function modalities = return_modalities(obj, subject, modality_group) % % Return the datatypes for a given for a given modality group for a given subject. @@ -129,17 +134,57 @@ end %% ENTITIES - function order = entity_order(obj, entity_list) + function order = entity_order(obj, varargin) % - % Returns the order of the entities in the list according to the BIDS official order. + % Returns the 'correct' order for entities of entity list. If there are + % non BIDS entities they are added after the BIDS ones in alphabetical + % order. % % USAGE:: % - % order = schema.entity_order(entity_list) + % order = schema.entity_order(entity_list) + % + % + % EXAMPLE:: + % + % schema = bids.Schema(); + % + % % get the order of all the BIDS entities + % order = schema.entity_order() + % + % + % % reorder typical BIDS entities + % entity_list_to_order = {'description' + % 'run' + % 'subject'}; + % order = schema.entity_order(entity_list_to_order) + % + % {'subject' + % 'run' + % 'description'}; % - % :param entity_list: List of entities - % :type entity_list: char or cellstr + % % reorder non-BIDS and typical BIDS entities + % entity_list_to_order = {'description' + % 'run' + % 'foo' + % 'subject'}; + % order = schema.entity_order(entity_list_to_order) % + % {'subject' + % 'run' + % 'description' + % 'foo'}; + % + + ischar_or_iscell = @(x) ischar(x) || iscellstr(x); + + if numel(varargin) == 1 + entity_list = varargin{1}; + elseif numel(varargin) == 0 + entity_list = fieldnames(obj.content.objects.entities); + end + + assert(ischar_or_iscell(entity_list)); if ischar(entity_list) entity_list = cellstr(entity_list); @@ -149,7 +194,33 @@ is_in_schema = ismember(order, entity_list); is_not_in_schema = ~ismember(entity_list, order); order = order(is_in_schema); - order = cat(1, order, entity_list(is_not_in_schema)); + order = cat(1, order, sort(entity_list(is_not_in_schema))); + + end + + function key = return_entity_key(obj, entity) + % + % Returns the key of an entity + % + % USAGE:: + % + % key = schema.return_entity_key(entity) + % + % EXAMPLE:: + % + % key = schema.return_entity_key('description') + % + % 'desc' + % + + if ~ismember(entity, fieldnames(obj.content.objects.entities)) + msg = sprintf('No entity ''%s'' in schema.\n Available entities are:\n- ', ... + entity, ... + strjoin(fieldnames(obj.content.objects.entities), '\n- ')); + bids.internal.error_handling(mfilename, 'UnknownEnitity', msg, false); + end + + key = obj.content.objects.entities.(entity).name; end @@ -227,6 +298,7 @@ end function entities = return_entities_for_suffix_group(obj, suffix_group) + % entities are returned in the expected order according to the schema % % USAGE:: % @@ -318,8 +390,6 @@ return end - % the following loop could probably be improved with some cellfun magic - % cellfun(@(x, y) any(strcmp(x,y)), {p.type}, suffix_groups) datatypes = obj.get_datatypes(); suffix_groups = obj.return_suffix_groups_for_datatype(modality); @@ -392,6 +462,10 @@ end end + + % in case we get duplicates + datatypes = unique(datatypes); + end function [entities, required] = return_entities_for_suffix_modality(obj, suffix, modality) @@ -472,6 +546,40 @@ reg_ex = ['^%s.*' suffixes extensions '$']; end + function [def, status] = get_definition(obj, word) + % + % finds definition of a column header in a the BIDS schema + % + % USAGE:: + % + % [def, status] = schema.get_definition(word) + % + + status = false; + if ~isfield(obj.content.objects, 'metadata') + obj.load_schema_metadata = true; + obj = obj.load(); + end + + if isfield(obj.content.objects.columns, word) + status = true; + def = obj.content.objects.columns.(word); + end + + if ~status && isfield(obj.content.objects.metadata, word) + status = true; + def = obj.content.objects.metadata.(word); + end + + if ~status + def = struct('LongName', word, ... + 'Description', 'TODO', ... + 'Units', 'TODO', ... + 'TermURL', 'TODO'); + end + + end + end % ----------------------------------------------------------------------- % diff --git a/+bids/bids_matlab_version.m b/+bids/bids_matlab_version.m index 121630b9..cc0b5e9b 100644 --- a/+bids/bids_matlab_version.m +++ b/+bids/bids_matlab_version.m @@ -1,7 +1,6 @@ function version_number = bids_matlab_version() % - % Reads the version number of the pipeline from the txt file in the root of the - % repository. + % Return bids matlab version. % % USAGE:: % @@ -9,6 +8,7 @@ % % :returns: :version_number: (string) Use semantic versioning format (like v0.1.0) % + % (C) Copyright 2021 BIDS-MATLAB developers try diff --git a/+bids/copy_to_derivative.m b/+bids/copy_to_derivative.m index 1e3a0369..e38a75e2 100644 --- a/+bids/copy_to_derivative.m +++ b/+bids/copy_to_derivative.m @@ -1,7 +1,6 @@ function copy_to_derivative(varargin) % - % Copy selected data from BIDS layout to given derivatives folder, - % returning layout of new derivatives folder + % Copy selected data from BIDS layout to given derivatives folder. % % USAGE:: % @@ -30,31 +29,32 @@ function copy_to_derivative(varargin) % % :param unzip: If ``true`` then all ``.gz`` files will be unzipped % after being copied. - % :type unzip: boolean + % :type unzip: logical % % :param force: If set to ``false`` it will not overwrite any file already % present in the destination. - % :type force: boolean + % :type force: logical % % :param skip_dep: If set to ``false`` it will copy all the % dependencies of each file. - % :type skip_dep: boolean + % :type skip_dep: logical % % :param tolerant: Defaults to ``false``. Set to ``true`` to turn errors into warnings. % :type tolerant: boolean % % :param use_schema: If set to ``true`` it will only copy files % that are BIDS valid. - % :type use_schema: boolean + % :type use_schema: logical % - % :param verbose: - % :type verbose: boolean + % :param verbose: + % :type verbose: logical % % All the metadata of each file is read through the whole hierarchy % and dumped into one side-car json file for each file copied. % In practice this "unravels" the inheritance principle. % % + % (C) Copyright 2021 BIDS-MATLAB developers default_pipeline_name = ''; @@ -90,9 +90,9 @@ function copy_to_derivative(varargin) subjects_list = bids.query(BIDS, 'subjects', args.Results.filter); if isempty(data_list) - disp(args.Results.filter); - msg = sprintf('No data found for this query in dataset:\n\t%s', ... - BIDS.pth); + msg = sprintf('No data found for this query:\t%s\n\nin dataset:\n\t%s', ... + bids.internal.create_unordered_list(args.Results.filter), ... + bids.internal.format_path(BIDS.pth)); bids.internal.error_handling(mfilename, 'noData', msg, ... args.Results.tolerant, ... args.Results.verbose); @@ -153,12 +153,11 @@ function copy_to_derivative(varargin) end -function copy_participants_tsv(BIDS, derivatives_folder, p) +function copy_participants_tsv(BIDS, derivatives_folder, args) % % Very "brutal" approach where we copy the whole file % - % TODO: - % - if only certain subjects are copied only copy those entries from the TSV + % TODO: if only certain subjects are copied only copy those entries from the TSV % if ~isempty(BIDS.participants) @@ -166,15 +165,15 @@ function copy_participants_tsv(BIDS, derivatives_folder, p) src = fullfile(BIDS.pth, 'participants.tsv'); target = fullfile(derivatives_folder, 'participants.tsv'); - copy_tsv(src, target, p); + copy_tsv(src, target, args); end end -function copy_tsv(src, target, p) +function copy_tsv(src, target, args) flag = false; - if p.Results.force + if args.Results.force flag = true; else if exist(target, 'file') == 0 @@ -183,29 +182,28 @@ function copy_tsv(src, target, p) end if flag - copy_with_symlink(src, target, p.Results.unzip, p.Results.verbose); + copy_with_symlink(src, target, args.Results.unzip, args.Results.verbose); if exist(bids.internal.file_utils(src, 'ext', '.json'), 'file') copy_with_symlink(bids.internal.file_utils(src, 'ext', '.json'), ... bids.internal.file_utils(target, 'ext', '.json'), ... - p.Results.unzip, ... - p.Results.verbose); + args.Results.unzip, ... + args.Results.verbose); end end end -function copy_session_scan_tsv(BIDS, derivatives_folder, p) +function copy_session_scan_tsv(BIDS, derivatives_folder, args) % % Very "brutal" approach where we copy the whole file % - % TODO: - % - only copy the entries of the sessions / files that are copied + % TODO: only copy the entries of the sessions / files that are copied % % identify in the BIDS layout the subjects / sessions combination that we % need to keep to copy - subjects_list = bids.query(BIDS, 'subjects', p.Results.filter); - sessions_list = bids.query(BIDS, 'sessions', p.Results.filter); + subjects_list = bids.query(BIDS, 'subjects', args.Results.filter); + sessions_list = bids.query(BIDS, 'sessions', args.Results.filter); subjects = {BIDS.subjects.name}'; subjects = cellfun(@(x) x(5:end), subjects, 'UniformOutput', false); @@ -221,7 +219,7 @@ function copy_session_scan_tsv(BIDS, derivatives_folder, p) target = fullfile(derivatives_folder, ... BIDS.subjects(keep(i)).name, ... bids.internal.file_utils(src, 'filename')); - copy_tsv(src, target, p); + copy_tsv(src, target, args); end if ~isempty(BIDS.subjects(keep(i)).scans) @@ -230,7 +228,7 @@ function copy_session_scan_tsv(BIDS, derivatives_folder, p) BIDS.subjects(keep(i)).name, ... BIDS.subjects(keep(i)).session, ... bids.internal.file_utils(src, 'filename')); - copy_tsv(src, target, p); + copy_tsv(src, target, args); end end @@ -260,7 +258,7 @@ function copy_file(BIDS, derivatives_folder, data_file, unzip_files, force, skip % avoid circular references if ~force && exist(fullfile(out_dir, file.filename), 'file') if verbose - fprintf(1, '\n skipping: %s', file.filename); + fprintf(1, '\n skipping: %s', bids.internal.format_path(file.filename)); end return else @@ -296,8 +294,7 @@ function copy_file(BIDS, derivatives_folder, data_file, unzip_files, force, skip function copy_with_symlink(src, target, unzip_files, verbose) % - % TODO: - % - test with actual datalad datasets on all OS + % TODO: test with actual datalad datasets on all OS % if verbose @@ -349,6 +346,11 @@ function use_copyfile(src, target, unzip_files, verbose) if ~status msg = [messageId ': ' message]; + if strcmp(messageId, 'MATLAB:COPYFILE:OSError') + msg = [msg, ... + '\n If you are on Windows and using a datalad dataset,', ... + '\n try to ''datalad unlock'' your input dataset.']; + end bids.internal.error_handling(mfilename, 'copyError', msg, false, verbose); end @@ -362,8 +364,7 @@ function copy_dependencies(file, BIDS, derivatives_folder, unzip, force, skip_de for dep = 1:numel(dependencies) - % % TODO - % % Dirty hack to prevent the copy of ASL data to crash here. + % % TODO Dirty hack to prevent the copy of ASL data to crash here. % % But this means that dependencies of ASL data will not be copied until % % this is fixed. % if ismember(dependencies{dep}, {'context', 'm0'}) @@ -379,7 +380,7 @@ function copy_dependencies(file, BIDS, derivatives_folder, unzip, force, skip_de copy_file(BIDS, derivatives_folder, dep_file, unzip, force, ~skip_dep, verbose); else - msg = sprintf('Dependency file %s not found', dep_file); + msg = sprintf('Dependency file %s not found', bids.internal.format_path(dep_file)); bids.internal.error_handling(mfilename, 'missingDependencyFile', msg, true, verbose); end diff --git a/+bids/derivatives_json.m b/+bids/derivatives_json.m index 24ec4406..2b454e17 100644 --- a/+bids/derivatives_json.m +++ b/+bids/derivatives_json.m @@ -10,8 +10,9 @@ % :type derivative_filename: string % :param force: when `true` it will force the creation of a json content even % when the filename contains no BIDS derivatives entity. - % :type force: boolean + % :type force: logical % + % (C) Copyright 2018 BIDS-MATLAB developers % @@ -67,11 +68,27 @@ %% entity related content if any(ismember(fieldnames(p.entities), 'res')) - content.Resolution = {{ struct(p.entities.res, 'REQUIRED if "res" entity') }}; + try + content.Resolution = {{ struct(p.entities.res, 'REQUIRED if "res" entity') }}; + catch + bids.internal.error_handling(mfilename(), 'invalidFieldname', .... + sprintf('Invalid field name for entity res-%s', ... + p.entities.res), ... + true, ... + true); + end end if any(ismember(fieldnames(p.entities), 'den')) - content.Density = {{ struct(p.entities.den, 'REQUIRED if "den" entity') }}; + try + content.Density = {{ struct(p.entities.den, 'REQUIRED if "den" entity') }}; + catch + bids.internal.error_handling(mfilename(), 'invalidFieldname', .... + sprintf('Invalid field name for entity den-%s', ... + p.entities.den), ... + true, ... + true); + end end %% suffix related content diff --git a/+bids/diagnostic.m b/+bids/diagnostic.m new file mode 100644 index 00000000..6b2b1eea --- /dev/null +++ b/+bids/diagnostic.m @@ -0,0 +1,293 @@ +function [diagnostic_table, sub_ses, headers] = diagnostic(varargin) + % + % Create a diagnostic figure for a dataset. + % + % - list the number of files for each subject split by: + % - modality + % - task (optional) + % - list the number of trials for each event type + % + % USAGE:: + % + % diagnostic_table = diagnostic(BIDS, ... + % 'use_schema', true, ... + % 'output_path', '', ... + % 'filter', struct(), ... + % 'split_by', {''}) + % + % :param BIDS: BIDS directory name or BIDS structure (from ``bids.layout``) + % :type BIDS: structure or string + % + % :param split_by: splits results by a given BIDS entity (now only ``task`` is supported) + % :type split_by: cell + % + % :param use_schema: If set to ``true``, the parsing of the dataset + % will follow the bids-schema provided with bids-matlab. + % If set to ``false`` files just have to be of the form + % ``sub-label_[entity-label]_suffix.ext`` to be parsed. + % If a folder path is provided, then the schema contained + % in that folder will be used for parsing. + % :type use_schema: logical + % + % :param out_path: path to directory containing the derivatives + % :type out_path: string + % + % :param filter: list of filters to choose what files to copy (see bids.query) + % :type filter: structure or cell + % + % :param trial_type_col: Optional. Name of the column containing the trial type. + % Defaults to ``'trial_type'``. + % :type trial_type_col: char + % + % :param verbose: Optional. Set to false to not show the figure. + % Defaults to ``true``. + % :type verbose: logical + % + % Examples:: + % + % BIDS = bids.layout(path_to_dataset); + % diagnostic_table = bids.diagnostic(BIDS, 'output_path', pwd); + % diagnostic_table = bids.diagnostic(BIDS, 'split_by', {'task'}, 'output_path', pwd); + % + % + + % (C) Copyright 2021 BIDS-MATLAB developers + + default_BIDS = pwd; + default_schema = false; + default_filter = struct(); + default_split = {''}; + default_output_path = ''; + default_verbose = true; + + args = inputParser; + + charOrStruct = @(x) ischar(x) || isstruct(x); + + addOptional(args, 'BIDS', default_BIDS, charOrStruct); + addParameter(args, 'use_schema', default_schema, @islogical); + addParameter(args, 'output_path', default_output_path, @ischar); + addParameter(args, 'filter', default_filter, @isstruct); + addParameter(args, 'split_by', default_split, @iscell); + addParameter(args, 'trial_type_col', 'trial_type', @ischar); + addParameter(args, 'verbose', default_verbose, @islogical); + + parse(args, varargin{:}); + + %% + BIDS = bids.layout(args.Results.BIDS, 'use_schema', args.Results.use_schema); + + output_path = args.Results.output_path; + + filter = args.Results.filter; + + subjects = bids.query(BIDS, 'subjects', filter); + + headers = get_headers(BIDS, filter, args.Results.split_by); + + diagnostic_table = nan(numel(subjects), numel(headers)); + + trial_type_col = args.Results.trial_type_col; + + verbose = args.Results.verbose; + + visible = 'on'; + if ~verbose + visible = 'off'; + end + + row = 1; + + %% + for i_sub = 1:numel(subjects) + + this_filter = get_clean_filter(filter, subjects{i_sub}); + + sessions = bids.query(BIDS, 'sessions', this_filter); + if isempty(sessions) + sessions = {''}; + end + + for i_sess = 1:numel(sessions) + + this_filter = get_clean_filter(filter, subjects{i_sub}); + this_filter.ses = sessions{i_sess}; + + files = bids.query(BIDS, 'data', this_filter); + + if size(files, 1) == 0 + continue + end + + for i_col = 1:numel(headers) + + this_filter = get_clean_filter(filter, subjects{i_sub}, sessions{i_sess}); + this_filter.modality = headers{i_col}.modality; + if isfield(headers{i_col}, 'task') + this_filter.task = headers{i_col}.task; + end + if isfield(headers{i_col}, 'suffix') + this_filter.suffix = headers{i_col}.suffix; + end + + files = bids.query(BIDS, 'data', this_filter); + + diagnostic_table(row, i_col) = size(files, 1); + + end + + sub_ses{row} = ['sub-' this_filter.sub]; + if ~isempty(this_filter.ses) + sub_ses{row} = ['sub-' this_filter.sub ' ses-' this_filter.ses]; + end + + row = row + 1; + + end + + end + + %% + fig_name = base_fig_name(BIDS); + if ~cellfun('isempty', args.Results.split_by) + fig_name = [base_fig_name(BIDS) ' - split_by ' strjoin(args.Results.split_by, '-')]; + end + + bids.internal.plot_diagnostic_table(diagnostic_table, headers, sub_ses, ... + strrep(fig_name, '_', ' '), ... + visible); + + print_figure(output_path, fig_name); + + if verbose + close(gcf); + end + %% events + modalities = bids.query(BIDS, 'modalities', filter); + tasks = bids.query(BIDS, 'tasks', filter); + + for i_modality = 1:numel(modalities) + + for i_task = 1:numel(tasks) + + [data, headers, y_labels] = bids.internal.list_events(BIDS, ... + modalities{i_modality}, ... + tasks{i_task}, ... + 'filter', filter, ... + 'trial_type_col', trial_type_col); + if isempty(data) + continue + end + + fig_name = [base_fig_name(BIDS), ' - ', modalities{i_modality}, ' - ', tasks{i_task}]; + fig_name = strrep(fig_name, '_', ' '); + + bids.internal.plot_diagnostic_table(data, ... + headers, ... + y_labels, ... + fig_name, ... + visible); + + print_figure(output_path, fig_name); + + end + + end + +end + +function fig_name = base_fig_name(BIDS) + fig_name = BIDS.description.Name; + if isempty(fig_name) || strcmp(fig_name, ' ') + fig_name = 'this_dataset'; + end +end + +function print_figure(output_path, fig_name) + if ~isempty(output_path) + + bids.util.mkdir(output_path); + + filename = regexprep([fig_name, '.png'], ' - ', '_'); + filename = regexprep(filename, ' ', '-'); + filename = regexprep(filename, '[\(\)]', ''); + filename = fullfile(output_path, filename); + + print(filename, '-dpng'); + fprintf('Figure saved:\n\t%s\n', filename); + + end +end + +function headers = get_headers(BIDS, filter, split_by) + % + % Get the headers to include in the output table + % + + % TODO will probably need to use a recursive way to build the header list + + headers = {}; + + modalities = bids.query(BIDS, 'modalities', filter); + + for i_modality = 1:numel(modalities) + + this_filter = filter; + this_filter.modality = modalities(i_modality); + + this_header = struct('modality', {modalities(i_modality)}); + + if ismember('suffix', split_by) + + suffixes = bids.query(BIDS, 'suffixes', this_filter); + + for i_suffix = 1:numel(suffixes) + + this_filter.suffix = suffixes(i_suffix); + + this_header.suffix = suffixes(i_suffix); + + if ismember('task', split_by) + headers = add_task_based_headers(BIDS, headers, this_filter, this_header, split_by); + else + headers{end + 1} = this_header; %#ok<*AGROW> + end + + end + + else + + if ismember('task', split_by) + headers = add_task_based_headers(BIDS, headers, this_filter, this_header, split_by); + else + headers{end + 1} = this_header; + end + + end + + end + +end + +function this_filter = get_clean_filter(filter, sub, ses) + this_filter = filter; + this_filter.sub = sub; + if nargin > 2 + this_filter.ses = ses; + end +end + +function headers = add_task_based_headers(BIDS, headers, this_filter, this_header, split_by) + + if ismember('task', split_by) + + tasks = bids.query(BIDS, 'tasks', this_filter); + + for i_task = 1:numel(tasks) + this_header.task = tasks(i_task); + headers{end + 1} = this_header; + end + + end + +end diff --git a/+bids/init.m b/+bids/init.m index 5724f817..4f3f74a7 100644 --- a/+bids/init.m +++ b/+bids/init.m @@ -7,7 +7,9 @@ function init(varargin) % bids.init(pth, ... % 'folders', folders, ,... % 'is_derivative', false,... - % 'is_datalad_ds', false) + % 'is_datalad_ds', false, ... + % 'tolerant', true, ... + % 'verbose', false) % % :param pth: directory where to create the dataset % :type pth: string @@ -19,12 +21,13 @@ function init(varargin) % :type folders: structure % % :param is_derivative: - % :type is_derivative: boolean + % :type is_derivative: logical % % :param is_datalad_ds: - % :type is_derivative: boolean + % :type is_derivative: logical % % + % (C) Copyright 2021 BIDS-MATLAB developers default.pth = pwd; @@ -32,61 +35,99 @@ function init(varargin) default.folders.subjects = ''; default.folders.sessions = ''; default.folders.modalities = ''; - + default_tolerant = true; + default_verbose = false; default.is_derivative = false; default.is_datalad_ds = false; - p = inputParser; + is_logical = @(x) islogical(x); + + args = inputParser; - addOptional(p, 'pth', default.pth, @ischar); - addParameter(p, 'folders', default.folders, @isstruct); - addParameter(p, 'is_derivative', default.is_derivative); - addParameter(p, 'is_datalad_ds', default.is_datalad_ds); + addOptional(args, 'pth', default.pth, @ischar); + addParameter(args, 'folders', default.folders, @isstruct); + addParameter(args, 'is_derivative', default.is_derivative); + addParameter(args, 'is_datalad_ds', default.is_datalad_ds); + addParameter(args, 'tolerant', default_tolerant, is_logical); + addParameter(args, 'verbose', default_verbose, is_logical); - parse(p, varargin{:}); + parse(args, varargin{:}); + + is_datalad_ds = args.Results.is_datalad_ds; + tolerant = args.Results.tolerant; + verbose = args.Results.verbose; %% Folder structure - if ~isempty(fieldnames(p.Results.folders)) + if ~isempty(fieldnames(args.Results.folders)) + + subjects = create_folder_names(args, 'subjects'); + if isfield(args.Results.folders, 'sessions') + sessions = create_folder_names(args, 'sessions'); + else + sessions = ''; + end - subjects = create_folder_names(p, 'subjects'); - sessions = create_folder_names(p, 'sessions'); + modalities = validate_folder_list(args, 'modalities'); - bids.util.mkdir(p.Results.pth, ... + bids.util.mkdir(args.Results.pth, ... subjects, ... sessions, ... - p.Results.folders.modalities); + modalities); else - bids.util.mkdir(p.Results.pth); + bids.util.mkdir(args.Results.pth); end - %% README - pth_to_readmes = fullfile(fileparts(mfilename('fullpath')), '..', 'templates'); - src = fullfile(pth_to_readmes, 'README'); - if p.Results.is_datalad_ds - src = fullfile(pth_to_readmes, 'README_datalad'); + if exist('subjects', 'var') && ~isempty(subjects) && ~isempty(subjects{1}) + bids.util.create_participants_tsv(args.Results.pth, 'use_schema', false, ... + 'verbose', verbose, ... + 'tolerant', tolerant); + if ~strcmp(sessions, '') + bids.util.create_sessions_tsv(args.Results.pth, 'use_schema', false, ... + 'verbose', verbose, ... + 'tolerant', tolerant); + end end - copyfile(src, fullfile(p.Results.pth, 'README')); + + bids.util.create_readme(args.Results.pth, is_datalad_ds, ... + 'verbose', verbose, ... + 'tolerant', tolerant); %% dataset_description ds_desc = bids.Description(); - ds_desc.is_derivative = p.Results.is_derivative; + ds_desc.is_derivative = args.Results.is_derivative; ds_desc = ds_desc.set_derivative; - ds_desc.write(p.Results.pth); + ds_desc.write(args.Results.pth); %% CHANGELOG - file_id = fopen(fullfile(p.Results.pth, 'CHANGES'), 'w'); + file_id = fopen(fullfile(args.Results.pth, 'CHANGES'), 'w'); fprintf(file_id, '1.0.0 %s\n', datestr(now, 'yyyy-mm-dd')); fprintf(file_id, '- dataset creation.'); fclose(file_id); end -function folder_list = create_folder_names(p, folder_level) +function folder_list = validate_folder_list(args, folder_level) - folder_list = p.Results.folders.(folder_level); + folder_list = args.Results.folders.(folder_level); if ~iscell(folder_list) folder_list = {folder_list}; end + folder_list(cellfun('isempty', folder_list)) = []; + + only_alphanum = regexp(folder_list, '^[0-9a-zA-Z]+$'); + if any(cellfun('isempty', only_alphanum)) + msg = sprintf('BIDS labels must be alphanumeric only. Got:\n\t%s', ... + bids.internal.create_unordered_list(folder_list)); + bids.internal.error_handling(mfilename(), ... + 'nonAlphaNumFodler', ... + msg, ... + false); + end +end + +function folder_list = create_folder_names(args, folder_level) + + folder_list = validate_folder_list(args, folder_level); switch folder_level case 'subjects' @@ -95,9 +136,10 @@ function init(varargin) prefix = 'ses-'; end - if ~isempty(p.Results.folders.(folder_level)) + if ~isempty(folder_list) + folder_list = cellfun(@(x) [prefix x], ... - p.Results.folders.(folder_level), ... + folder_list, ... 'UniformOutput', false); end diff --git a/+bids/layout.m b/+bids/layout.m index 31b44624..e46d96c1 100644 --- a/+bids/layout.m +++ b/+bids/layout.m @@ -7,6 +7,8 @@ % BIDS = bids.layout(pwd, ... % 'use_schema', true, ... % 'index_derivatives', false, ... + % 'index_dependencies', true, ... + % 'filter', struct([]), ... % 'tolerant', true, ... % 'verbose', false) % @@ -20,21 +22,43 @@ % ``sub-label_[entity-label]_suffix.ext`` to be parsed. % If a folder path is provided, then the schema contained % in that folder will be used for parsing. - % :type use_schema: boolean + % :type use_schema: logical % % :param index_derivatives: if ``true`` this will index the content of the % any ``derivatives`` folder in the BIDS dataset. - % :type index_derivatives: boolean + % :type index_derivatives: logical + % + % :param index_dependencies: if ``true`` this will index the dependencies (json files, + % associated TSV files for each file...) + % :type index_dependencies: logical + % + % :param filter: if ``true`` this will index the dependencies (json files, + % associated TSV files for each file...) + % :type filter: struct with optional fields ``sub``, ``ses``, ``modality``. + % Regular expression can be used for ``sub`` and ``ses``. % % :param tolerant: Set to ``true`` to turn validation errors into warnings - % :type tolerant: boolean + % :type tolerant: logical % % :param verbose: Set to ``true`` to get more feedback - % :type verbose: boolean + % :type verbose: logical + % + % + % Example:: % + % BIDS = bids.layout(fullfile(get_test_data_dir(), '7t_trt'), ... + % 'use_schema', true, ... + % 'verbose', true, ... + % 'index_derivatives', false, ... + % 'filter', struct('sub', {{'^0[12]'}}, ... + % 'modality', {{'anat', 'func'}}, ... + % 'ses', {{'1', '2'}})); % + % + % (C) Copyright 2016-2018 Guillaume Flandin, Wellcome Centre for Human Neuroimaging % + % (C) Copyright 2018 BIDS-MATLAB developers %% Validate input arguments @@ -42,16 +66,20 @@ default_root = pwd; default_index_derivatives = false; + default_index_dependencies = true; default_tolerant = true; + default_filter = struct([]); default_use_schema = true; default_verbose = false; - isDirOrStruct = @(x) (isstruct(x) || isdir(x)); + is_dir_or_struct = @(x) (isstruct(x) || isdir(x)); args = inputParser(); - addOptional(args, 'root', default_root, isDirOrStruct); + addOptional(args, 'root', default_root, is_dir_or_struct); addParameter(args, 'index_derivatives', default_index_derivatives); + addParameter(args, 'index_dependencies', default_index_dependencies); + addParameter(args, 'filter', default_filter, @isstruct); addParameter(args, 'tolerant', default_tolerant); addParameter(args, 'use_schema', default_use_schema); addParameter(args, 'verbose', default_verbose); @@ -60,6 +88,8 @@ root = args.Results.root; index_derivatives = args.Results.index_derivatives; + index_dependencies = args.Results.index_dependencies; + filter = args.Results.filter; tolerant = args.Results.tolerant; use_schema = args.Results.use_schema; verbose = args.Results.verbose; @@ -76,17 +106,21 @@ end + if verbose + fprintf(1, '\n\nIndexing dataset:\n\t%s\n', bids.internal.format_path(root)); + end + %% BIDS structure % ========================================================================== - % BIDS.dir -- BIDS directory - % BIDS.description -- content of dataset_description.json - % BIDS.sessions -- cellstr of sessions - % BIDS.participants -- for participants.tsv - % BIDS.subjects -- structure array of subjects - % BIDS.root -- tsv and json files in the root folder - - BIDS = struct( ... - 'pth', root, ... + % BIDS.dir -- BIDS directory + % BIDS.is_datalad_ds -- BIDS directory + % BIDS.description -- content of dataset_description.json + % BIDS.sessions -- cellstr of sessions + % BIDS.participants -- for participants.tsv + % BIDS.subjects -- structure array of subjects + % BIDS.root -- tsv and json files in the root folder + + BIDS = struct('pth', root, ... 'description', struct([]), ... 'sessions', {{}}, ... 'participants', struct([]), ... @@ -112,28 +146,47 @@ subjects = cellstr(bids.internal.file_utils('List', BIDS.pth, 'dir', '^sub-.*$')); if isequal(subjects, {''}) msg = sprintf('No subjects found in BIDS directory: ''%s''', ... - BIDS.pth); + bids.internal.format_path(BIDS.pth)); bids.internal.error_handling(mfilename, 'noSubject', msg, tolerant, verbose); return end + BIDS.is_datalad_ds = false; + if isdir(fullfile(BIDS.pth, '.datalad')) && isdir(fullfile(BIDS.pth, '.git')) + BIDS.is_datalad_ds = true; + end + schema = bids.Schema(use_schema); schema.verbose = verbose; for iSub = 1:numel(subjects) + + if exclude_subject(filter, strrep(subjects{iSub}, 'sub-', '')) + continue + end + + if verbose + fprintf(1, ' Indexing subject: %s [', subjects{iSub}); + end + sessions = cellstr(bids.internal.file_utils('List', ... fullfile(BIDS.pth, subjects{iSub}), ... 'dir', ... '^ses-.*$')); for iSess = 1:numel(sessions) + + if exclude_session(filter, strrep(sessions{iSess}, 'ses-', '')) + continue + end + if isempty(BIDS.subjects) BIDS.subjects = parse_subject(BIDS.pth, subjects{iSub}, sessions{iSess}, ... - schema, tolerant, verbose); + schema, filter, tolerant, verbose); else new_subject = parse_subject(BIDS.pth, subjects{iSub}, sessions{iSess}, ... - schema, tolerant, verbose); + schema, filter, tolerant, verbose); [BIDS.subjects, new_subject] = bids.internal.match_structure_fields(BIDS.subjects, ... new_subject); % TODO: this can be added to "match_structure_fields" @@ -143,9 +196,13 @@ end + if verbose + fprintf(1, ']\n'); + end + end - BIDS = manage_dependencies(BIDS, verbose); + BIDS = manage_dependencies(BIDS, index_dependencies, verbose); BIDS = index_derivatives_dir(BIDS, index_derivatives, verbose); @@ -156,6 +213,34 @@ end +function value = exclude(filter, entity, label) + value = false; + % skip if not included in filter + if ~isfield(filter, entity) + return + end + % use regex when filter is a cell of numel 1 + if numel(filter.(entity)) == 1 + if cellfun('isempty', regexp(label, filter.(entity))) + value = true; + end + return + end + % otherwise we just check all elements + if ~ismember(label, filter.(entity)) + value = true; + return + end +end + +function value = exclude_subject(filter, sub_label) + value = exclude(filter, 'sub', sub_label); +end + +function value = exclude_session(filter, ses_label) + value = exclude(filter, 'ses', ses_label); +end + function BIDS = index_root_directory(BIDS) % index json and tsv files in the root directory files_to_exclude = {'participants', ... already done @@ -195,8 +280,7 @@ '.*')); for iDir = 1:numel(der_folders) - BIDS.derivatives.(der_folders{iDir}) = bids.layout( ... - fullfile(BIDS.pth, ... + BIDS.derivatives.(der_folders{iDir}) = bids.layout(fullfile(BIDS.pth, ... 'derivatives', ... der_folders{iDir}), ... 'use_schema', false, ... @@ -208,7 +292,7 @@ end end -function subject = parse_subject(pth, subjname, sesname, schema, tolerant, verbose) +function subject = parse_subject(pth, subjname, sesname, schema, filter, tolerant, verbose) % % Parse a subject's directory % @@ -243,7 +327,14 @@ % if we go schema-less, we pass an empty schema.content to all the parsing functions % so the parsing is unconstrained for iModality = 1:numel(modalities) + + if isfield(filter, 'modality') && ... + ~ismember(modalities{iModality}, filter.modality) + continue + end + switch modalities{iModality} + case {'anat', ... 'func', ... 'beh', ... @@ -256,11 +347,17 @@ 'perf', ... 'micr', ... 'nirs'} + subject = parse_using_schema(subject, modalities{iModality}, schema, verbose); + otherwise + + if isempty(modalities{iModality}) + continue + end + % in case we are going schemaless % or the modality is not one of the usual suspect - if ~bids.internal.is_valid_fieldname(modalities{iModality}) msg = sprintf('subject ''%s'' contains an invalid subfolder ''%s''. Skipping.', ... subject.path, ... @@ -273,6 +370,7 @@ subject.(modalities{iModality}) = struct([]); subject = parse_using_schema(subject, modalities{iModality}, schema, verbose); + end end @@ -286,6 +384,10 @@ if exist(pth, 'dir') + if verbose + fprintf(1, '.'); + end + subject = bids.internal.add_missing_field(subject, modality); file_list = return_file_list(modality, subject, schema); @@ -331,8 +433,7 @@ aslcontext_file = strrep(subject.perf(end).filename, ... ['_asl' subject.perf(end).ext], ... '_aslcontext.tsv'); - subject.(modality)(end).dependencies.context = manage_tsv( ... - struct('content', [], ... + subject.(modality)(end).dependencies.context = manage_tsv(struct('content', [], ... 'meta', []), ... pth, ... aslcontext_file, ... @@ -354,15 +455,18 @@ if ~exist(fullfile(BIDS.pth, 'dataset_description.json'), 'file') - msg = sprintf('BIDS directory not valid: missing dataset_description.json: ''%s''', ... - BIDS.pth); + msg = sprintf(['BIDS directory not valid: missing dataset_description.json: ''%s''', ... + '\nSee this section of the BIDS specification:\n\t%s\n'], ... + bids.internal.format_path(BIDS.pth), ... + bids.internal.url('description')); bids.internal.error_handling(mfilename, 'missingDescripton', msg, tolerant, verbose); end try BIDS.description = bids.util.jsondecode(fullfile(BIDS.pth, 'dataset_description.json')); catch err - msg = sprintf('BIDS dataset description could not be read: %s', err.message); + msg = sprintf('BIDS dataset description could not be read:\n %s', ... + bids.internal.format_path(err.message)); bids.internal.error_handling(mfilename, 'cannotReadDescripton', msg, tolerant, verbose); end @@ -370,14 +474,14 @@ for iField = 1:numel(fields_to_check) if ~isfield(BIDS.description, fields_to_check{iField}) - msg = sprintf( ... - 'BIDS dataset description not valid: missing %s field.', ... - fields_to_check{iField}); + msg = sprintf(['BIDS dataset description not valid: missing %s field.', ... + 'See this section of the BIDS specification:\n\t%s\n'], ... + fields_to_check{iField}, ... + bids.internal.url('description')); bids.internal.error_handling(mfilename, 'invalidDescripton', msg, tolerant, verbose); end - % TODO - % Add warning if bids version does not match schema version + % TODO Add warning if bids version does not match schema version end @@ -412,12 +516,10 @@ % - can include a prefix % - can be json - % TODO - % it should be possible to create some of those patterns for the regexp + % TODO it should be possible to create some of those patterns for the regexp % based on some of the required entities written down in the schema - % TODO - % this does not cover coordsystem.json + % TODO this does not cover coordsystem.json % prefix only for shemaless data if isempty(schema.content) @@ -550,7 +652,35 @@ ['^' strrep(filename, ['.' ext], ['\.' ext]) '$']); if isempty(tsv_file) - msg = sprintf('Missing: %s', fullfile(pth, filename)); + + missing_file = bids.internal.file_utils(filename, 'basename'); + append = ''; + switch missing_file + case 'participants' + msg = sprintf(['Missing: %s', ... + '\n\n', ... + 'To silence this warning, ', ... + 'consider adding a "participants.tsv" to your dataset.', ... + '\n', ... + 'See the function: bids.util.create_participants_tsv\n', ... + 'See also this section of the BIDS specification:\n\t%s'], ... + bids.internal.format_path(fullfile(pth, filename)), ... + bids.internal.url('participants')); + case 'samples' + msg = sprintf(['Missing: %s', ... + '\n\n', ... + 'To silence this warning, ', ... + 'consider adding a "samples.tsv" to your dataset.', ... + '\n', ... + 'See this section of the BIDS specification:\n\t%s'], ... + bids.internal.format_path(fullfile(pth, filename)), ... + bids.internal.url('samples')); + otherwise + msg = sprintf(' Missing: %s%s', ... + bids.internal.format_path(fullfile(pth, filename)), ... + append); + end + bids.internal.error_handling(mfilename, 'tsvMissing', msg, tolerant, verbose); else @@ -567,11 +697,15 @@ end -function BIDS = manage_dependencies(BIDS, verbose) +function BIDS = manage_dependencies(BIDS, index_dependencies, verbose) % % Loops over all files and retrieve all files that current file depends on % + if ~index_dependencies + return + end + tolerant = true; file_list = bids.query(BIDS, 'data'); @@ -598,11 +732,23 @@ end for iIntended = 1:numel(intended) + dest = fullfile(BIDS.pth, BIDS.subjects(info_src.sub_idx).name, ... intended{iIntended}); + % TODO: need to better take care of URI + if strfind(intended{iIntended}, ':') + tmp = strsplit(intended{iIntended}, '/'); + this_intended = strjoin(tmp(2:end), '/'); + dest = fullfile(BIDS.pth, BIDS.subjects(info_src.sub_idx).name, this_intended); + end + + % only throw warning for non-datalad dataset + % to avoid excessive warning as sym link are not files if ~exist(dest, 'file') - msg = ['IntendedFor file ' dest ' from ' file.filename ' not found']; - bids.internal.error_handling(mfilename, 'IntendedForMissing', msg, tolerant, verbose); + if ~BIDS.is_datalad_ds + msg = ['IntendedFor file ' dest ' from ' file.filename ' not found']; + bids.internal.error_handling(mfilename, 'IntendedForMissing', msg, tolerant, verbose); + end continue end info_dest = bids.internal.return_file_info(BIDS, dest); @@ -627,7 +773,7 @@ if ~isfield(perf.meta, 'M0Type') - msg = sprintf('M0Type field missing for %s', perf.filename); + msg = sprintf('M0Type field missing for %s', bids.internal.format_path(perf.filename)); bids.internal.error_handling(mfilename, 'm0typeMissing', msg, tolerant, verbose); else diff --git a/+bids/query.m b/+bids/query.m index 68cc745c..a208e90a 100644 --- a/+bids/query.m +++ b/+bids/query.m @@ -146,9 +146,8 @@ % % data = bids.query(BIDS, 'data', 'task', '') % - % + % (C) Copyright 2016-2018 Guillaume Flandin, Wellcome Centre for Human Neuroimaging - % % (C) Copyright 2018 BIDS-MATLAB developers %#ok<*AGROW> @@ -174,7 +173,7 @@ bids.internal.error_handling(mfilename(), 'unknownQuery', msg, false, true); end - bids_entities = schema_entities(); + % bids_entities = schema_entities(); BIDS = bids.layout(BIDS); @@ -197,7 +196,7 @@ % Get optional target option for metadata query [target, options] = get_target(query, options); - result = perform_query(BIDS, query, options, subjects, modalities, target, bids_entities); + result = perform_query(BIDS, query, options, subjects, modalities, target); %% Postprocessing output variable switch query @@ -325,6 +324,12 @@ case cat(2, {'suffixes', 'suffixes', 'extensions', 'prefixes'}, valid_entity_qu subjects = options{ismember(options(:, 1), 'sub'), 2}; options(ismember(options(:, 1), 'sub'), :) = []; else + if ~isfield(BIDS, 'subjects') || ~isfield(BIDS.subjects, 'name') + msg = sprintf(['No subject present in dataset:\n\t%s.', ... + '\nDid you run bids.layout first?'], ... + bids.internal.format_path(BIDS.pth)); + bids.internal.error_handling(mfilename(), 'noSubjectField', msg, false); + end subjects = unique({BIDS.subjects.name}); subjects = regexprep(subjects, '^[a-zA-Z0-9]+-', ''); end @@ -363,7 +368,7 @@ case cat(2, {'suffixes', 'suffixes', 'extensions', 'prefixes'}, valid_entity_qu end -function result = perform_query(BIDS, query, options, subjects, modalities, target, bids_entities) +function result = perform_query(BIDS, query, options, subjects, modalities, target) % Initialise output variable result = {}; @@ -388,13 +393,13 @@ case cat(2, {'suffixes', 'suffixes', 'extensions', 'prefixes'}, valid_entity_qu end result = update_result(query, options, result, this_subject, ... - this_modality, target, bids_entities); + this_modality, target); end end - result = update_result_with_root_content(query, options, result, BIDS, bids_entities); + result = update_result_with_root_content(query, options, result, BIDS); end @@ -409,7 +414,6 @@ case cat(2, {'suffixes', 'suffixes', 'extensions', 'prefixes'}, valid_entity_qu this_subject = varargin{4}; this_modality = varargin{5}; target = varargin{6}; - bids_entities = varargin{7}; d = this_subject.(this_modality); @@ -452,7 +456,7 @@ case cat(2, {'suffixes', 'suffixes', 'extensions', 'prefixes'}, valid_entity_qu try result{end} = subsref(result{end}, target); catch - warning('Non-existent field for metadata.'); + warning('Non-existent field "%s" for metadata.', target.subs); result{end} = []; end end @@ -463,7 +467,7 @@ case cat(2, {'suffixes', 'suffixes', 'extensions', 'prefixes'}, valid_entity_qu case valid_entity_queries() - result = update_if_entity(query, result, d(k), bids_entities); + result = update_if_entity(query, result, d(k)); case {'suffixes', 'prefixes'} field = query(1:end - 2); @@ -481,7 +485,7 @@ case valid_entity_queries() end end -function result = update_result_with_root_content(query, options, result, BIDS, bids_entities) +function result = update_result_with_root_content(query, options, result, BIDS) d = BIDS.root; @@ -515,7 +519,7 @@ case valid_entity_queries() case valid_entity_queries() - result = update_if_entity(query, result, d(k), bids_entities); + result = update_if_entity(query, result, d(k)); case {'suffixes', 'prefixes'} field = query(1:end - 2); @@ -535,7 +539,7 @@ case valid_entity_queries() value = schema.content.objects.entities; end -function result = update_if_entity(query, result, dk, bids_entities) +function result = update_if_entity(query, result, dk) if ismember(query, short_valid_entity_queries()) field = query(1:end - 1); @@ -544,6 +548,8 @@ case valid_entity_queries() field = 'atlas'; elseif ismember(query, long_valid_entity_queries()) + + bids_entities = schema_entities(); field = bids_entities.(query(1:end - 1)).name; else diff --git a/+bids/report.m b/+bids/report.m index 59eb0583..d15df6e1 100644 --- a/+bids/report.m +++ b/+bids/report.m @@ -14,22 +14,27 @@ % % :param BIDS: Path to BIDS dataset or output of bids.layout [Default = pwd] % :type BIDS: string or structure + % % :param filter: Specifies which the subject, session, ... to take as template. % [Default = struct('sub', '', 'ses', '')]. See bids.query % for more information. % :type filter: structure + % % :param output_path: Folder where the report should be printed. If empty % (default) then the output is sent to the prompt. % :type output_path: string + % % :param read_nifti: If set to ``true`` (default) the function will try to read the % NIfTI file to get more information. This relies on the % ``spm_vol.m`` function from SPM. - % :type read_nifti: boolean + % :type read_nifti: logical + % % :param verbose: If set to ``false`` (default) the function does not % output anything to the prompt. - % :type verbose: boolean + % :type verbose: logical % % + % (C) Copyright 2018 BIDS-MATLAB developers % TODO @@ -39,7 +44,7 @@ % - report summary statistics on participants as suggested in COBIDAS report % - check if all subjects have the same content? % - take care of other recommended metafield in BIDS specs or COBIDAS? - % - add a dataset description (ethics, grant, scanner details...) + % - add a dataset description (ethics, grant...) default_BIDS = pwd; default_filter = struct('sub', '', 'ses', ''); @@ -47,32 +52,37 @@ default_read_nifti = false; default_verbose = false; - p = inputParser; + args = inputParser; charOrStruct = @(x) ischar(x) || isstruct(x); - addOptional(p, 'BIDS', default_BIDS, charOrStruct); - addParameter(p, 'output_path', default_output_path, @ischar); - addParameter(p, 'filter', default_filter, @isstruct); - addParameter(p, 'read_nifti', default_read_nifti); - addParameter(p, 'verbose', default_verbose); + addOptional(args, 'BIDS', default_BIDS, charOrStruct); + addParameter(args, 'output_path', default_output_path, @ischar); + addParameter(args, 'filter', default_filter, @isstruct); + addParameter(args, 'read_nifti', default_read_nifti); + addParameter(args, 'verbose', default_verbose); + + parse(args, varargin{:}); - parse(p, varargin{:}); + schema = bids.Schema(); - BIDS = bids.layout(p.Results.BIDS); + BIDS = bids.layout(args.Results.BIDS); - filter = check_filter(BIDS, p); + filter = check_filter(BIDS, args); nb_sub = numel(filter.sub); - read_nii = p.Results.read_nifti & exist('spm_vol', 'file') == 2; + read_nii = args.Results.read_nifti & exist('spm_vol', 'file') == 2; - [file_id, filename] = open_output_file(BIDS, p.Results.output_path, p.Results.verbose); + [file_id, filename] = open_output_file(BIDS, args.Results.output_path, args.Results.verbose); - if p.Results.verbose + if args.Results.verbose fprintf(1, '\n%s\n', repmat('-', 80, 1)); end + text = '\n# Data description\n'; + print_to_output(text, file_id, args.Results.verbose); + for i_sub = 1:nb_sub this_filter = filter; @@ -89,8 +99,8 @@ this_filter.ses = sessions{i_sess}; if numel(sessions) > 1 - text = sprintf('\n Working on session: %s\n', this_filter.ses); - print_to_output(text, file_id, p.Results.verbose); + text = sprintf('\n ## session %s\n', this_filter.ses); + print_to_output(text, file_id, args.Results.verbose); end modalities = bids.query(BIDS, 'modalities', this_filter); @@ -99,26 +109,28 @@ this_filter.modality = modalities(i_modality); - print_to_output([upper(this_filter.modality{1}) ' REPORT'], ... + desc = schema.content.objects.datatypes.(this_filter.modality{1}).display_name; + + print_to_output(['### ' desc ' data'], ... file_id, ... - p.Results.verbose); + args.Results.verbose); switch this_filter.modality{1} case {'anat', 'perf', 'dwi', 'fmap', 'pet'} - report_nifti(BIDS, this_filter, read_nii, p.Results.verbose, file_id); + report_nifti(BIDS, this_filter, read_nii, args.Results.verbose, file_id); case {'func'} - report_func(BIDS, this_filter, read_nii, p.Results.verbose, file_id); + report_func(BIDS, this_filter, read_nii, args.Results.verbose, file_id); case {'eeg', 'meg', 'ieeg'} - report_meeg(BIDS, this_filter, p.Results.verbose, file_id); + report_meeg(BIDS, this_filter, args.Results.verbose, file_id); case {'beh'} - not_supported(this_filter.modality{1}, p.Results.verbose); + not_supported(this_filter.modality{1}, args.Results.verbose); otherwise - not_supported(this_filter.modality{1}, p.Results.verbose); + not_supported(this_filter.modality{1}, args.Results.verbose); end @@ -128,7 +140,7 @@ end - print_text('credit', file_id, p.Results.verbose); + print_text('credit', file_id, args.Results.verbose); end @@ -139,6 +151,16 @@ function report_nifti(BIDS, filter, read_nii, verbose, file_id) for iType = 1:numel(suffixes) filter.suffix = suffixes{iType}; + + schema = bids.Schema(); + try + suffix_fullname = schema.content.objects.suffixes.(filter.suffix).display_name; + catch + suffix_fullname = 'UNKNOWN'; + end + + print_to_output(['#### ' suffix_fullname], file_id, verbose); + boilerplate = get_boilerplate(filter.modality{1}, verbose); if ismember(filter.suffix, {'blood', 'asllabeling'}) @@ -206,15 +228,15 @@ function report_func(BIDS, filter, read_nii, verbose, file_id) for iType = 1:numel(suffixes) filter.suffix = suffixes{iType}; - boilerplate = get_boilerplate(filter.modality{1}, verbose); - if ismember(filter.suffix, {'physio'}) - not_supported(filter.suffix, verbose); - continue + if ~ismember(filter.suffix, {'physio', 'events'}) + print_to_output(['#### ' upper(filter.suffix) ' data'], file_id, verbose); end - % events are taken care of as part by print_events_info below - if ismember(filter.suffix, {'events'}) + boilerplate = get_boilerplate(filter.modality{1}, verbose); + + % events and physio are taken care of as part by print_X_info below + if ismember(filter.suffix, {'events', 'physio'}) continue end @@ -223,10 +245,13 @@ function report_func(BIDS, filter, read_nii, verbose, file_id) % add mention of contrast for iTask = 1:numel(tasks) + print_to_output(['##### Task ' tasks{iTask} ' data'], file_id, verbose); + + this_filter = filter; this_filter.task = tasks{iTask}; - [filter, nb_runs] = update_filter_with_run_label(BIDS, filter); + [this_filter, nb_runs] = update_filter_with_run_label(BIDS, this_filter); - [~, metadata] = get_filemane_and_metadata(BIDS, filter); + [~, metadata] = get_filemane_and_metadata(BIDS, this_filter); boilerplate = bids.internal.replace_placeholders(boilerplate, metadata); acq_param = get_acq_param(BIDS, this_filter, read_nii, verbose); @@ -244,7 +269,9 @@ function report_func(BIDS, filter, read_nii, verbose, file_id) print_text('task', file_id, verbose, acq_param); - print_events_info(file_id, BIDS, filter, verbose); + print_events_info(file_id, BIDS, this_filter, verbose); + + print_physio_info(file_id, BIDS, this_filter, verbose); end @@ -260,6 +287,8 @@ function report_meeg(BIDS, filter, verbose, file_id) filter.suffix = suffixes{iType}; + print_to_output(['#### ' upper(filter.suffix) ' data'], file_id, verbose); + boilerplate = get_boilerplate(filter.modality{1}, verbose); if ismember(filter.suffix, {'physio', 'channels', 'headshape'}) @@ -276,6 +305,8 @@ function report_meeg(BIDS, filter, verbose, file_id) for iTask = 1:numel(tasks) + print_to_output(['##### Task ' tasks{iTask} ' data'], file_id, verbose); + filter.task = tasks{iTask}; [filter, nb_runs] = update_filter_with_run_label(BIDS, filter); @@ -316,9 +347,9 @@ function not_supported(thing_not_supported, verbose) verbose); end -function filter = check_filter(BIDS, p) +function filter = check_filter(BIDS, args) - filter = p.Results.filter; + filter = args.Results.filter; if ~isfield(filter, 'sub') filter.sub = ''; @@ -383,7 +414,7 @@ function not_supported(thing_not_supported, verbose) else - text = sprintf('Dataset description saved in: %s\n\n', filename); + text = sprintf('Dataset description saved in: %s\n\n', bids.internal.format_path(filename)); print_to_output(text, 1, verbose); end @@ -433,10 +464,9 @@ function not_supported(thing_not_supported, verbose) end function param = get_acq_param(BIDS, filter, read_gz, verbose) - % Will get info from acquisition parameters from the BIDS structure or from - % the NIfTI files + % Will get info from acquisition parameters + % from the BIDS structure or from the NIfTI files - % acq_param = set_default_acq_param(); param = struct(); [filename, metadata] = get_filemane_and_metadata(BIDS, filter); @@ -460,8 +490,7 @@ function not_supported(thing_not_supported, verbose) param.echo_time = [metadata.EchoTime1 metadata.EchoTime2]; end - % TODO - % acq_param = convert_field_to_millisecond(acq_param, {'te'}); + % TODO acq_param = convert_field_to_millisecond(acq_param, {'te'}); if isfield(metadata, 'EchoTime1') && isfield(metadata, 'EchoTime2') param.echo_time = sprintf('%0.2f / %0.2f', param.echo_time); @@ -469,6 +498,8 @@ function not_supported(thing_not_supported, verbose) param = convert_field_to_str(param); + param = define_multiband(param); + param = define_slice_timing(param); param = read_nifti(read_gz, filename{1}, param, verbose); @@ -504,11 +535,12 @@ function not_supported(thing_not_supported, verbose) acq_param.vox_size = sprintf('%.2f X %.2f X %.2f', vs(1), vs(2), vs(3)); % field of view - acq_param.fov = sprintf('%.2f X %.2f', vs(1) * dim(1), vs(2) * dim(2)); + acq_param.fov = sprintf('%.0f X %.0f', round(vs(1) * dim(1)), round(vs(2) * dim(2))); catch - msg = sprintf('Could not read the header from file %s.\n', filename); + msg = sprintf('Could not read the header from file %s.\n', ... + bids.internal.format_path(filename)); bids.internal.error_handling(mfilename, 'cannotReadHeader', msg, true, verbose); end @@ -525,7 +557,7 @@ function not_supported(thing_not_supported, verbose) for iField = 1:size(fields_list, 1) - if isfield(metadata, fields_list{iField}) + if isfield(metadata, fields_list{iField, 2}) acq_param.(fields_list{iField, 1}) = metadata.(fields_list{iField, 2}); end @@ -545,18 +577,47 @@ function not_supported(thing_not_supported, verbose) end -function acq_param = define_slice_timing(acq_param) +function acq_param = define_multiband(acq_param) if ~isfield(acq_param, 'so_str') || isempty(acq_param.so_str) return end so_str = acq_param.so_str; + if iscell(so_str) + so_str = cell2mat(so_str); + end + if ischar(so_str) + so_str = cellstr(so_str); + so_str = str2double(so_str); + end + + % assume that all unique values of the slice time order + % are repeated the same number of time + tmp = unique(so_str); + mb_str = sum(so_str == tmp(1)); + + if mb_str > 1 + acq_param.mb_str = mb_str; + end + +end + +function acq_param = define_slice_timing(acq_param) + + if ~isfield(acq_param, 'so_str') || isempty(acq_param.so_str) + return + end % Try to figure out the order the slices were acquired from their timing + so_str = acq_param.so_str; if iscell(so_str) so_str = cell2mat(so_str); end + if ischar(so_str) + so_str = cellstr(so_str); + so_str = str2double(so_str); + end [~, I] = sort(so_str); @@ -591,7 +652,7 @@ function print_to_output(text, file_id, verbose) if bids.internal.is_octave() bids.internal.error_handling(mfilename(), ... 'notImplemented', ... - 'saving to file not implement for Ocatve', ... + 'Saving to file not implemented for Octave.\n', ... true, verbose); else fprintf(file_id, text); @@ -638,12 +699,36 @@ function print_events_info(file_id, BIDS, filter, verbose) filter.suffix = 'events'; [~, metadata] = get_filemane_and_metadata(BIDS, filter); - if isfield(metadata, 'StimulusPresentation') - print_text('events', file_id, verbose, metadata); + + if ~isempty(metadata) + print_to_output('###### events data', file_id, verbose); + + if isfield(metadata, 'StimulusPresentation') + print_text('events', file_id, verbose, metadata); + end + if isfield(metadata, 'trial_type') + % TODO + not_supported('trial_type description', verbose); + end end - if isfield(metadata, 'trial_type') - % TODO - not_supported('trial_type description', verbose); + +end + +function print_physio_info(file_id, BIDS, filter, verbose) + + filter.suffix = 'physio'; + + [~, metadata] = get_filemane_and_metadata(BIDS, filter); + + if ~isempty(metadata) + print_to_output('###### physiological data', file_id, verbose); + + if isfield(metadata, 'Columns') + metadata.Columns = strjoin(metadata.Columns, ', '); + print_text('physio', file_id, verbose, metadata); + end + + print_text('device_info', file_id, verbose, metadata); end end diff --git a/+bids/transformers.m b/+bids/transformers.m new file mode 100644 index 00000000..8fa4b603 --- /dev/null +++ b/+bids/transformers.m @@ -0,0 +1,250 @@ +function [new_content, json] = transformers(varargin) + % + % Apply transformers to a structure + % + % USAGE:: + % + % new_content = transformers(trans, data) + % + % :param transformers: + % :type transformers: structure + % + % :param data: + % :type data: structure + % + % :returns: - :new_content: (structure) + % - :json: (structure) json equivalent of the transformers + % + % EXAMPLE:: + % + % data = bids.util.tsvread(path_to_tsv); + % + % % load transformation instruction from a model file + % bm = bids.Model('file', model_file); + % transformers = bm.get_transformations('Level', 'Run'); + % + % % apply transformers + % new_content = bids.transformers(transformers.Instructions, data); + % + % % if all fields in the structure have the same number of rows one + % % create a new tsv file + % bids.util.tsvwrite(path_to_new_tsv, new_content) + % + % + % See also: bids.Model + % + % + + % (C) Copyright 2022 BIDS-MATLAB developers + + SUPPORTED_TRANSFORMERS = lower(cat(1, basic_transfomers, ... + munge_transfomers, ... + logical_transfomers, ... + compute_transfomers)); + + p = inputParser; + + isStructOrCell = @(x) isstruct(x) || iscell(x); + + addRequired(p, 'trans', isStructOrCell); + addRequired(p, 'data', @isstruct); + + parse(p, varargin{:}); + + data = p.Results.data; + trans = p.Results.trans; + + json = struct('Transformer', ['bids-matlab_' bids.internal.get_version], ... + 'Instructions', trans); + if iscell(trans) + json = struct('Transformer', ['bids-matlab_' bids.internal.get_version], ... + 'Instructions', {trans}); + end + + if isempty(trans) || isempty(data) + new_content = data; + return + end + + % apply all the transformers sequentially + for iTrans = 1:numel(trans) + + if iscell(trans) + this_transformer = trans{iTrans}; + elseif isstruct(trans) + this_transformer = trans(iTrans); + end + + if ~ismember(lower(this_transformer.Name), SUPPORTED_TRANSFORMERS) + not_implemented(this_transformer.Name); + return + end + + data = apply_transformer(this_transformer, data); + new_content = data; + + end + +end + +function output = apply_transformer(trans, data) + + transformerName = lower(trans.Name); + + if ~isfield(trans, 'verbose') + end + + switch transformerName + + case lower(basic_transfomers) + output = bids.transformers_list.Basic(trans, data); + + case lower(logical_transfomers) + output = bids.transformers_list.Logical(trans, data); + + case lower(munge_transfomers) + output = apply_munge(trans, data); + + case lower(compute_transfomers) + output = apply_compute(trans, data); + + otherwise + not_implemented(trans.Name); + + end + +end + +function output = apply_munge(trans, data) + + transformerName = lower(trans.Name); + + switch transformerName + + case 'assign' + output = bids.transformers_list.Assign(trans, data); + + case 'concatenate' + output = bids.transformers_list.Concatenate(trans, data); + + case 'constant' + output = bids.transformers_list.Constant(trans, data); + + case 'copy' + output = bids.transformers_list.Copy(trans, data); + + case 'delete' + output = bids.transformers_list.Delete(trans, data); + + case 'dropna' + output = bids.transformers_list.Drop_na(trans, data); + + case 'factor' + output = bids.transformers_list.Factor(trans, data); + + case 'filter' + output = bids.transformers_list.Filter(trans, data); + + case 'labelidenticalrows' + output = bids.transformers_list.Label_identical_rows(trans, data); + + case 'mergeidenticalrows' + output = bids.transformers_list.Merge_identical_rows(trans, data); + + case 'rename' + output = bids.transformers_list.Rename(trans, data); + + case 'select' + output = bids.transformers_list.Select(trans, data); + + case 'replace' + output = bids.transformers_list.Replace(trans, data); + + case 'split' + output = bids.transformers_list.Split(trans, data); + + otherwise + + not_implemented(transformer.Name); + + end + +end + +function output = apply_compute(trans, data) + + transformerName = lower(trans.Name); + + switch transformerName + + case 'sum' + output = bids.transformers_list.Sum(trans, data); + + case 'product' + output = bids.transformers_list.Product(trans, data); + + case 'mean' + output = bids.transformers_list.Mean(trans, data); + + case 'stddev' + output = bids.transformers_list.Std(trans, data); + + case 'scale' + output = bids.transformers_list.Scale(trans, data); + + case 'threshold' + output = bids.transformers_list.Threshold(trans, data); + + otherwise + + not_implemented(trans.Name); + + end + +end + +function not_implemented(name) + bids.internal.error_handling(mfilename(), 'notImplemented', ... + sprintf('Transformer %s not implemented', name), ... + false); +end + +function BASIC = basic_transfomers() + BASIC = {'Add' + 'Divide' + 'Multiply' + 'Power' + 'Subtract'}; +end + +function LOGICAL = logical_transfomers() + LOGICAL = {'And' + 'Or' + 'Not'}; +end + +function MUNGE = munge_transfomers() + MUNGE = {'Assign' + 'Concatenate' + 'Constant' + 'Copy' + 'Delete' + 'DropNA' + 'Filter' + 'Factor' + 'LabelIdenticalRows' + 'MergeIdenticalRows' + 'Rename' + 'Replace' + 'Select' + 'Split'}; +end + +function COMPUTE = compute_transfomers() + COMPUTE = {'Mean' + 'Product' + 'Scale' + 'StdDev' + 'Sum' + 'Threshold'}; +end diff --git a/+bids/validate.m b/+bids/validate.m index a431529a..d405b627 100644 --- a/+bids/validate.m +++ b/+bids/validate.m @@ -1,9 +1,10 @@ -function [sts, msg] = validate(root) +function [sts, msg] = validate(root, options) + % % BIDS Validator % % USAGE:: % - % [sts, msg] = bids.validate(root) + % [sts, msg] = bids.validate(root, options) % % :param root: directory formatted according to BIDS [Default: pwd] % :type string: @@ -21,13 +22,19 @@ % % % + % (C) Copyright 2018 Guillaume Flandin, Wellcome Centre for Human Neuroimaging % + % (C) Copyright 2018 BIDS-MATLAB developers + if nargin < 2 + options = ''; + end + [sts, ~] = system('bids-validator --version'); if sts msg = 'Require bids-validator from https://github.com/bids-standard/bids-validator'; else - [sts, msg] = system(['bids-validator "' strrep(root, '"', '\"') '"']); + [sts, msg] = system(['bids-validator ' options ' "' strrep(root, '"', '\"') '"']); end diff --git a/.codespellrc b/.codespellrc index 1e8626e5..82787b09 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,4 +1,4 @@ [codespell] skip = *.js,*.svg,*.eps,.git,env,*build,bids-examples,coverage_html,schema.json -ignore-words-list = te,ans +ignore-words-list = te,ans,nin builtin = clear,rare diff --git a/.github/workflows/miss_hit.yml b/.github/workflows/miss_hit.yml new file mode 100644 index 00000000..b5032c7d --- /dev/null +++ b/.github/workflows/miss_hit.yml @@ -0,0 +1,45 @@ +name: miss_hit + +on: + push: + branches: + - main + - dev + paths: + - '**.m' + pull_request: + branches: ['*'] + paths: + - '**.m' + +jobs: + + miss_hit: + + runs-on: ubuntu-latest + + strategy: + matrix: + command: ["mh_style", "mh_metric --ci && mh_lint"] + fail-fast: true # cancel all jobs if one fails + + steps: + + - uses: actions/checkout@v3 + with: + submodules: true + fetch-depth: 1 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools + pip3 install -r requirements.txt + + - name: ${{ matrix.command }} + run: | + ${{ matrix.command }} diff --git a/.github/workflows/run_examples.yml b/.github/workflows/run_examples.yml new file mode 100644 index 00000000..55cc4768 --- /dev/null +++ b/.github/workflows/run_examples.yml @@ -0,0 +1,46 @@ +name: tests_examples + +on: + push: + branches: + - master + - dev + paths: + - "**.m" + - .github/workflows/*.yml + - schema.json + pull_request: + branches: ["*"] + paths: + - "**.m" + - .github/workflows/*.yml + - schema.json + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-22.04 + + steps: + - name: Install MATLAB + uses: matlab-actions/setup-matlab@v1.2.1 + with: + release: R2020a + + - name: Clone bids-matlab + uses: actions/checkout@v3 + with: + submodules: true + fetch-depth: 1 + + - name: Install bids example + run: | + cd demos/notebooks + make install + + - name: Run commands + uses: matlab-actions/run-command@v1.1.0 + with: + command: cd('demos/notebooks'); success = test_notebooks(); assert(success); diff --git a/.github/workflows/run_tests_matlab.yml b/.github/workflows/run_tests_matlab.yml index f45e6efc..17b394e6 100644 --- a/.github/workflows/run_tests_matlab.yml +++ b/.github/workflows/run_tests_matlab.yml @@ -13,9 +13,17 @@ on: jobs: test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: + + - uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Install bids validator + run: npm install -g bids-validator + - name: Install MATLAB uses: matlab-actions/setup-matlab@v1.2.3 with: diff --git a/.github/workflows/run_tests_octave.yml b/.github/workflows/run_tests_octave.yml index 5269ce27..048599dd 100644 --- a/.github/workflows/run_tests_octave.yml +++ b/.github/workflows/run_tests_octave.yml @@ -7,8 +7,6 @@ on: - dev paths: - '**.m' - - .github/workflows/*.yml - - schema.json pull_request: branches: ["*"] paths: @@ -22,7 +20,7 @@ env: jobs: test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: @@ -31,6 +29,9 @@ jobs: sudo apt-get -y -qq update sudo apt-get -y install octave liboctave-dev + - name: Install bids validator + run: npm install -g bids-validator + - name: Clone bids-matlab uses: actions/checkout@v3 with: @@ -43,7 +44,7 @@ jobs: cd JSONio mkoctfile --mex jsonread.c jsmn.c -DJSMN_PARENT_LINKS - - name: Install bids-example and data + - name: Install bids-example run: | cd tests make data @@ -51,17 +52,7 @@ jobs: - name: MOxUnit Action uses: joergbrech/moxunit-action@v1.2.0 with: - tests: tests # files or directories containing the MOxUnit test cases - src: +bids # directories to be added to the Octave search path before running the tests. - ext: JSONio tests/utils # External resources to add to the search put (excluded from coverage) - # data: # Directory for test data + tests: tests + src: +bids + ext: JSONio tests/utils with_coverage: false - # cover_xml_file: coverage.xml - - # - name: Code coverage - # uses: codecov/codecov-action@v1 - # with: - # file: coverage.xml # optional - # flags: unittests # optional - # name: codecov-umbrella # optional - # fail_ci_if_error: true # optional (default = false) diff --git a/.gitignore b/.gitignore index 9c0df628..104596f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,13 @@ -octave-workspace -*.asv -*.*~ *.swp tmp* -examples/brainstorm -examples/MoAEpilot +*.zip* + +*.jpg +*.ps + +*.log # virtual env env/* @@ -15,21 +16,63 @@ env/* bids-specification/* bids-examples +# examples +MoAEpilot + # input and output data involved in tests -data/* derivatives/* -tests/*.tsv + +# virtual env +env/* # coverage coverage_html +coverage.xml # styling .prettierrc -# visual studio code config -.vscode/* +# doc +build + +# jupyter notebooks +.ipynb_checkpoints + +## MATLAB / OCTAVE gitignore template + +# From : https://github.com/github/gitignore/blob/master/Global/MATLAB.gitignore -MoAEpilot.zip +# Windows default autosave extension +*.asv + +# OSX / *nix default autosave extension +*.m~ + +# Compiled MEX binaries (all platforms) +*.mex* + +# Packaged app and toolbox files +*.mlappinstall +*.mltbx + +# Generated helpsearch folders +helpsearch*/ + +# Simulink code generation folders +slprj/ +sccprj/ + +# Matlab code generation folders +codegen/ + +# Simulink autosave extension +*.autosave + +# Simulink cache files +*.slxc + +# Octave session info +octave-workspace # javascript node_modules/* diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..9e80844a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "json.schemas": [ + { + "fileMatch": ["model-*_smdl.json"], + "url": "https://raw.githubusercontent.com/bids-standard/stats-models/gh-pages/BIDSStatsModel.json" + } + ] +} diff --git a/CITATION.cff b/CITATION.cff index ae894ba2..f5a94027 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -4,7 +4,7 @@ cff-version: 1.2.0 title: bids-matlab -version: 0.1.0 +version: 0.1.0dev license: GPL-3.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 07547f67..10505695 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -102,12 +102,12 @@ overall project's goals and immediate next steps. The current list of labels are [here](https://github.com/bids-standard/bids-matlab/labels) and include: -- [![Opinions wanted](https://img.shields.io/badge/-opinions%20wanted-84b6eb.svg)](https://github.com/bids-standard/bids-matlab/labels/opinions%20wanted) - _These issues hold discussions where we're especially eager for feedback._ +- [![Opinions wanted](https://img.shields.io/badge/-opinions%20wanted-84b6eb.svg)](https://github.com/bids-standard/bids-matlab/labels/opinions%20wanted) + _These issues hold discussions where we're especially eager for feedback._ - Ongoing discussions benefit from broad feedback. This label is used to - highlight issues where decisions are being considered, so please join the - conversation! + Ongoing discussions benefit from broad feedback. This label is used to + highlight issues where decisions are being considered, so please join the + conversation! @@ -186,11 +186,13 @@ There is a [pre-commit hook](https://pre-commit.com/) that you can use to reformat files as you commit them. Install pre-commit by using our `requirements.txt` file + ```bash pip install -r requirements.txt ``` Install the hook + ```bash pre-commit install ``` @@ -245,13 +247,13 @@ stable version of the toolbox in the `main` branch and the latest version in the Use one of the following prefixes in the title of your pull request: -- `[ENH]` - enhancement of the software that adds a new feature or support for - a new data type -- `[FIX]` - fix of a bug or documentation error -- `[INFRA]` - changes to the infrastructure automating the project release - (for example, testing in continuous integration, building HTML docs) -- `[MISC]` - everything else including changes to the file listing - contributors +- `[ENH]` - enhancement of the software that adds a new feature or support for + a new data type +- `[FIX]` - fix of a bug or documentation error +- `[INFRA]` - changes to the infrastructure automating the project release + (for example, testing in continuous integration, building HTML docs) +- `[MISC]` - everything else including changes to the file listing + contributors If you are opening a pull request to obtain early feedback, but the changes are not ready to be merged (also known as a "work in progress" pull request, @@ -260,8 +262,8 @@ sometimes abbreviated by `WIP`), please use a If your pull request include: -- some new features in the code base -- or if it changes the expected behavior of the code that is already in place, +- some new features in the code base +- or if it changes the expected behavior of the code that is already in place, you may be asked to provide tests to describe the new expected behavior of the code. @@ -298,22 +300,20 @@ This section outlines how to comment on a pull request. The list of pull requests can be found by clicking on the "Pull requests" tab in the [BIDS-MATLAB repository](https://github.com/bids-standard/BIDS-MATLAB). -![BIDS-mainpage](commenting_images/BIDS_GitHub_mainpage.png "BIDS_GitHub_mainpage") - ### Pull request description Upon opening the pull request we see a detailed description of what this pull request is seeking to address. Descriptions are important for reviewers and the community to gain context into what the pull request is achieving. -![BIDS-pr](commenting_images/BIDS_pr.png "BIDS_pr") +![BIDS-pr](docs/commenting_images/BIDS_pr.png "BIDS_pr") ### Generally commenting on a pull request At the bottom of the pull request page, a comment box is provided for general comments and questions. -![BIDS-comment](commenting_images/BIDS_comment.png "BIDS-comment") +![BIDS-comment](docs/commenting_images/BIDS_comment.png "BIDS-comment") ### Specific comments on a pull request @@ -324,7 +324,7 @@ before each removed line. To comment on a specific line, hover over it, and click the blue plus sign (pictured below). Multiple lines can be selected by clicking and dragging the plus sign. -![BIDS-specific-comment](commenting_images/BIDS_file_comment.png "BIDS-specific-comment") +![BIDS-specific-comment](docs/commenting_images/BIDS_file_comment.png "BIDS-specific-comment") #### Suggesting text @@ -332,19 +332,19 @@ Comments on lines can contain "suggestions", which allow you to propose specific wording for consideration. To make a suggestion, click the plus/minus (±) icon in the comment box (pictured below). -![BIDS-suggest-box](commenting_images/BIDS_suggest.png "BIDS-suggest") +![BIDS-suggest-box](docs/commenting_images/BIDS_suggest.png "BIDS-suggest") Once the button is clicked the highlighted text will be copied into the comment box and formatted as a [Markdown code block](https://help.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks). -![BIDS-suggest-text](commenting_images/BIDS_suggest_text.png "BIDS-suggest-box") +![BIDS-suggest-text](docs/commenting_images/BIDS_suggest_text.png "BIDS-suggest-box") The "Preview" tab in the comment box will show your suggestion as it will be rendered. The "Suggested change" box will highlight the differences between the original text and your suggestion. -![BIDS-suggest-change](commenting_images/BIDS_suggest_change.png "BIDS-suggest-change") +![BIDS-suggest-change](docs/commenting_images/BIDS_suggest_change.png "BIDS-suggest-change") A comment may be submitted on its own by clicking "Add single comment". Several comments may be grouped by clicking "Start a review". As more comments are @@ -363,7 +363,7 @@ their contribution. To do this, you must click on the `Files changed` tab at the top of the page of a pull request. -![BIDS_pr_files_changed](commenting_images/BIDS_pr_files_changed.png "BIDS_pr_files_changed") +![BIDS_pr_files_changed](docs/commenting_images/BIDS_pr_files_changed.png "BIDS_pr_files_changed") From there you can browse the different files changed and the 'diff' for each of them (what line was changed and what the change consist of). You can also see @@ -371,18 +371,18 @@ comments and directly change suggestions made by reviewers. You can add each suggestion one by one or group them together in a batch. -![BIDS_pr_accept_comment](commenting_images/BIDS_pr_accept_comment.png "BIDS_pr_accept_comment") +![BIDS_pr_accept_comment](docs/commenting_images/BIDS_pr_accept_comment.png "BIDS_pr_accept_comment") If you decide to batch the suggestions to add several of them at once, you must scroll back to the top of the 'Files changed' page and the `commit suggestions` button will let you add all those suggestions as a single commit. -![BIDS_pr_commit_batch](commenting_images/BIDS_pr_commit_batch.png "BIDS_pr_commit_batch") +![BIDS_pr_commit_batch](docs/commenting_images/BIDS_pr_commit_batch.png "BIDS_pr_commit_batch") Once those suggestions are committed the commit information should mention the reviewer as a co-author. -![BIDS_pr_reviewer_credit](commenting_images/BIDS_pr_reviewer_credit.png "BIDS_pr_reviewer_credit") +![BIDS_pr_reviewer_credit](docs/commenting_images/BIDS_pr_reviewer_credit.png "BIDS_pr_reviewer_credit") ## How the decision to merge a pull request is made? diff --git a/DECISION-MAKING.md b/DECISION-MAKING.md index 5e87903f..f0e5f1b2 100644 --- a/DECISION-MAKING.md +++ b/DECISION-MAKING.md @@ -43,7 +43,6 @@ The rules outlined below are inspired by the and heavily depend on the [GitHub Pull Request Review system](https://help.github.com/articles/about-pull-requests/). - ## Rules 1. Every modification of the specification (including a correction of a typo, @@ -103,12 +102,13 @@ The `main` branch holds the stable version of the toolbox. The `dev` branch is where the latest version can be fetched. Version bumps and new releases are triggered: + - by hotfixes of bug - by a merge of the develop branch in the main branch. A diagram version of the decision-making flow we are aiming for is shown below. ([source](https://blog.axosoft.com/gitflow/)) -![git_flow](commenting_images/gitflow_diagram.png "gitflow_diagram") +![git_flow](./docs/commenting_images/gitflow_diagram.png) #### Conditions for merge into `dev` @@ -125,5 +125,6 @@ Conditions: Eventually though this technical debt must be paid back before a new release and merging into the main branch. Conditions: + - All unit and integration tests must pass. - All checks for code style and quality must pass. diff --git a/README.md b/README.md index d1c1c892..bec4d6f5 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,35 @@ -[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/bids-standard/bids-matlab/master.svg)](https://results.pre-commit.ci/latest/github/bids-standard/bids-matlab/master) + + + + +[![tests_matlab](https://github.com/bids-standard/bids-matlab/actions/workflows/run_tests_matlab.yml/badge.svg)](https://github.com/bids-standard/bids-matlab/actions/workflows/run_tests_matlab.yml) +[![tests_octave](https://github.com/bids-standard/bids-matlab/actions/workflows/run_tests_octave.yml/badge.svg)](https://github.com/bids-standard/bids-matlab/actions/workflows/run_tests_octave.yml) +[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/bids-standard/bids-matlab/dev?urlpath=demos) +[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/bids-standard/bids-matlab/dev.svg)](https://results.pre-commit.ci/latest/github/bids-standard/bids-matlab/dev) [![miss hit](https://img.shields.io/badge/code%20style-miss_hit-000000.svg)](https://misshit.org/) -[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/bids-standard/bids-matlab/master) +[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/bids-standard/bids-matlab/dev?urlpath=demos) [![View bids-matlab on File Exchange](https://www.mathworks.com/matlabcentral/images/matlab-file-exchange.svg)](https://nl.mathworks.com/matlabcentral/fileexchange/93740-bids-matlab) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.5910584.svg)](https://doi.org/10.5281/zenodo.5910584) [![All Contributors](https://img.shields.io/badge/all_contributors-19-orange.svg?style=flat-square)](#contributors-) + + +- [BIDS for MATLAB / Octave](#bids-for-matlab--octave) + - [Installation](#installation) + - [Get the latest features](#get-the-latest-features) + - [Features](#features) + - [What this toolbox can do](#what-this-toolbox-can-do) + - [What this toolbox cannot do... yet](#what-this-toolbox-cannot-do-yet) + - [What will this toolbox most likely never do](#what-will-this-toolbox-most-likely-never-do) + - [Usage](#usage) + - [Demos](#demos) + - [Requirements](#requirements) + - [Reading and writing JSON files](#reading-and-writing-json-files) + - [Implementation](#implementation) + - [Get in touch](#get-in-touch) + - [Other tools (MATLAB only)](#other-tools-matlab-only) + - [Contributing](#contributing) + # BIDS for MATLAB / Octave This repository aims at centralising MATLAB/Octave tools to interact with @@ -12,10 +37,6 @@ BIDS (Brain Imaging Data Structure) datasets. For more information about BIDS, visit https://bids.neuroimaging.io/. -Join our chat on the -[BIDS-MATLAB channel](https://mattermost.brainhack.org/brainhack/channels/bids-matlab) -on the brainhack mattermost and our [google group](https://groups.google.com/g/bids-matlab). - See also [PyBIDS](https://github.com/bids-standard/pybids) for Python and the [BIDS Starter Kit](https://github.com/bids-standard/bids-starter-kit). @@ -86,8 +107,9 @@ git checkout upstream/dev anatomical MRI, functional MRI, diffusion weighted imaging, field map data (see `bids.report`) -- create summary figures listing the number of files for each subject / session and - and imaging modality (see `bids.diagnostic`) +- `bids.diagnostic` creates summary figures listing: + - the number of files for each subject / session and imaging modality + - the number of trials for each trial type in each events.tsv file for a given task - read and write JSON files (see `bids.util.jsondecode` and `bids.util.jsonwrite`) provided that the right @@ -138,10 +160,9 @@ To use the `+bids/+util/jsondecode.m` function: content = bids.util.jsondecode('/home/data/some_json_file.json'); ``` -A -[tutorial](https://github.com/bids-standard/bids-matlab/blob/master/examples/tutorial.ipynb) -is available as a Jupyter Notebook and can be run interactively via -[Binder](https://mybinder.org/v2/gh/bids-standard/bids-matlab/master?filepath=examples/tutorial.ipynb). +## Demos + +There are demos and tutorials showing some of the features in the `demos` folder. ## Requirements @@ -156,7 +177,12 @@ this is not guaranteed. ### Reading and writing JSON files -If you are using MATLAB R2016b or newer, nothing else needs to be installed. +Make sure to be familiar with the [JSON 101](https://bids-standard.github.io/stats-models/json_101.html). + +Note some of the perks of working with JSON files described +on [the BIDS starterkit](https://bids-standard.github.io/bids-starter-kit/folders_and_files/metadata.html#interoperability-issues). + +For BIDS-MATLAB, if you are using MATLAB R2016b or newer, nothing else needs to be installed. If you are using MATLAB R2016a or older, or using Octave, you need to install a supported JSON library for your MATLAB or Octave. This can be any of: @@ -171,6 +197,16 @@ Starting point was `spm_BIDS.m` from [SPM12](https://github.com/spm/spm12) reformatted in a `+bids` package with dependencies to other SPM functions removed. +## Get in touch + +To contact us: + +- open an [issue](https://github.com/bids-standard/bids-matlab/issues/new/choose) +- join our chat on the +[BIDS-MATLAB channel](https://mattermost.brainhack.org/brainhack/channels/bids-matlab) +on the brainhack mattermost +- join our [google group](https://groups.google.com/g/bids-matlab). + ## Other tools (MATLAB only) - [dicm2nii](https://github.com/xiangruili/dicm2nii): A DICOM to BIDS @@ -185,7 +221,10 @@ removed. [resting state analysis from OMEGA datasets](https://neuroimage.usc.edu/brainstorm/Tutorials/RestingOmega#BIDS_specifications) ) -## Contributors ✨ +## Contributing + +If you want to contribute make sure to check our [contributing guidelines](CONTRIBUTING.md) +and our [code of conduct](CODE_OF_CONDUCT.md). Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): diff --git a/binder/postBuild b/binder/postBuild index c596e2e9..240a5a90 100644 --- a/binder/postBuild +++ b/binder/postBuild @@ -10,6 +10,6 @@ cd .. octave --no-gui --no-window-system --silent --eval "addpath (fullfile (getenv (\"HOME\"), \"JSONio\")); savepath ();" -cd "${HOME}/examples" +cd "${HOME}/demos/notebooks" make install diff --git a/demos/.gitignore b/demos/.gitignore new file mode 100644 index 00000000..5e73d3e3 --- /dev/null +++ b/demos/.gitignore @@ -0,0 +1,13 @@ + +*/*/README +*/*/CHANGES +*/*/sub* +*/*/sourcedata +*/*/derivatives + +spm/moae/MoAEpilot +spm/facerep/raw + +*.mat +*.nii* +*json diff --git a/demos/README.md b/demos/README.md new file mode 100644 index 00000000..6efa950c --- /dev/null +++ b/demos/README.md @@ -0,0 +1,33 @@ +## Notebooks + +[Tutorials](https://github.com/bids-standard/bids-matlab/blob/master/demos/notebooks/tutorial.ipynb) +are available as a Jupyter Notebook to be run with Octave and that can be run interactively via +[Binder](https://mybinder.org/v2/gh/bids-standard/bids-matlab/master?filepath=examples/tutorial.ipynb). + +There is also `.m` script equivalent for each tutorial that can be run with MATLAB. + +## SPM + +This shows how to use BIDS-MATLAB with SPM12 by running some of the tutorials +from the SPM website by relying on BIDS-MATLAB for file querying. + +There is also an example of how to extract confound information from fmriprep datasets +to make it easier to analyse them with SPM. + +The output should have a BIDS like structure like this: + +```bash +spm12 +├── CHANGES +├── dataset_description.json +├── README +└── sub-01 + └── stats + ├── sub-01_task-facerepetition_confounds.mat + └── sub-01_task-facerepetition_confounds.tsv +``` + +## Transformers + +Small demo on how to use transformers to modify the content of events TSV files +to be used with the BIDS statistical model. diff --git a/demos/fmap_failexemple/dataset_description.json b/demos/fmap_failexemple/dataset_description.json new file mode 100755 index 00000000..465a9304 --- /dev/null +++ b/demos/fmap_failexemple/dataset_description.json @@ -0,0 +1,4 @@ +{ + "Name": "FiledMap test", + "BIDSVersion": "1.9.3" +} diff --git a/demos/fmap_failexemple/participants.json b/demos/fmap_failexemple/participants.json new file mode 100755 index 00000000..d5372c50 --- /dev/null +++ b/demos/fmap_failexemple/participants.json @@ -0,0 +1,26 @@ +{ + "participant_id": { + "Description": "Unique participant label" + }, + "Age": { + "Description": "Participant age (at MR baseline scan)", + "Units": "years" + }, + "Gender": { + "Description": "Participant gender", + "Levels": { + "M": "male", + "F": "female" + } + }, + "Drug": { + "Description": "Drug intervention", + "Levels": { + "PSI": "Psilocybin" + } + }, + "DOB": { + "Description": "Date of birth", + "Units": "datetime" + } +} diff --git a/demos/fmap_failexemple/participants.tsv b/demos/fmap_failexemple/participants.tsv new file mode 100755 index 00000000..b0da31f7 --- /dev/null +++ b/demos/fmap_failexemple/participants.tsv @@ -0,0 +1,4 @@ +participant_id Age Gender Drug DOB +sub-55809 33.97672827 F PSI 27-Aug-86 +sub-57460 28.90896646 M PSI 25-Jul-92 +sub-57507 26.57631759 M PSI 9-Feb-95 diff --git a/demos/fmap_failexemple/sub-55809/ses-Baseline/fmap/sub-55809_ses-Baseline_acq-se_dir-AP_run-1_epi.json b/demos/fmap_failexemple/sub-55809/ses-Baseline/fmap/sub-55809_ses-Baseline_acq-se_dir-AP_run-1_epi.json new file mode 100644 index 00000000..f83a27db --- /dev/null +++ b/demos/fmap_failexemple/sub-55809/ses-Baseline/fmap/sub-55809_ses-Baseline_acq-se_dir-AP_run-1_epi.json @@ -0,0 +1,73 @@ +{ + "Modality": "MR", + "MagneticFieldStrength": 3, + "ImagingFrequency": 123.251, + "Manufacturer": "Siemens", + "ManufacturersModelName": "Prisma", + "InstitutionName": "Hospital", + "InstitutionalDepartmentName": "Department", + "InstitutionAddress": "Blegdamsvej 9,Copenhagen,District,DK,2100", + "DeviceSerialNumber": "166141", + "StationName": "AWP166141", + "BodyPartExamined": "BRAIN", + "PatientPosition": "HFS", + "ProcedureStepDescription": "NP2^Head", + "SoftwareVersions": "syngo MR E11", + "MRAcquisitionType": "2D", + "SeriesDescription": "SpinEchoFieldMap_AP", + "ProtocolName": "SpinEchoFieldMap_AP", + "ScanningSequence": "EP", + "SequenceVariant": "SK", + "ScanOptions": "FS", + "SequenceName": "epse2d1_104", + "ImageType": [ + "ORIGINAL", + "PRIMARY", + "M", + "ND", + "MOSAIC" + ], + "NonlinearGradientCorrection": false, + "SeriesNumber": 9, + "AcquisitionTime": "10:43:0.420000", + "AcquisitionNumber": 1, + "SliceThickness": 2, + "SpacingBetweenSlices": 2, + "SAR": 0.148123, + "EchoTime": 0.066, + "RepetitionTime": 8, + "FlipAngle": 90, + "PartialFourier": 1, + "BaseResolution": 104, + "ShimSetting": [55,-11471,8076,384,34,175,-6,-9], + "TxRefAmp": 240.18, + "PhaseResolution": 1, + "VendorReportedEchoSpacing": 0.00058, + "ReceiveCoilName": "Head_32", + "ReceiveCoilActiveElements": "HEA;HEP", + "PulseSequenceDetails": "%CustomerSeq%\\cmrr_mbep2d_se", + "WipMemBlock": "5ce7e86e-9d8c-496f-83b4-8afa6c31171c||Sequence: R016 ve11e/master r/434b28f1; Aug 7 2019 19:20:16 by eja", + "CoilCombinationMethod": "Sum of Squares", + "ConsistencyInfo": "N4_VE11E_LATEST_20181129", + "MatrixCoilMode": "SENSE", + "PercentPhaseFOV": 100, + "PercentSampling": 100, + "EchoTrainLength": 104, + "PhaseEncodingSteps": 104, + "AcquisitionMatrixPE": 104, + "ReconMatrixPE": 104, + "BandwidthPerPixelPhaseEncode": 16.578, + "EffectiveEchoSpacing": 0.000580009, + "DerivedVendorReportedEchoSpacing": 0.000580009, + "TotalReadoutTime": 0.0597409, + "PixelBandwidth": 2290, + "DwellTime": 2.1e-06, + "PhaseEncodingDirection": "j-", + "SliceTiming": [3.99,0,4.1,0.11,4.21,0.22,4.3225,0.3325,4.4325,0.4425,4.5425,0.5525,4.655,0.665,4.765,0.775,4.875,0.885,4.9875,0.9975000000000001,5.0975,1.1075,5.2075,1.2175,5.32,1.33,5.43,1.44,5.54,1.55,5.6525,1.6625,5.7625,1.7725,5.8725,1.8825,5.985,1.995,6.095,2.105,6.205,2.215,6.3175,2.3275,6.4275,2.4375,6.5375,2.5475,6.65,2.66,6.76,2.77,6.87,2.88,6.9825,2.9925,7.0925,3.1025,7.2025,3.2125,7.315,3.325,7.425,3.435,7.535,3.545,7.6475,3.6575,7.7575,3.7675,7.8675,3.8775], + "ImageOrientationPatientDICOM": [0.999947,0.0102972,-4.68781e-05,-0.0102973,0.999937,-0.00455223], + "ImageOrientationText": "Tra>Cor(-0.3)", + "InPlanePhaseEncodingDirectionDICOM": "COL", + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20220720", + "generatedfrom": "SPINECHOFIELDMAP_AP_0009" +} diff --git a/tests/data/SurfaceData/sub-06_hemi-R_space-individual_den-native_midthickness.surf.gii b/demos/fmap_failexemple/sub-55809/ses-Baseline/fmap/sub-55809_ses-Baseline_acq-se_dir-AP_run-1_epi.nii.gz similarity index 100% rename from tests/data/SurfaceData/sub-06_hemi-R_space-individual_den-native_midthickness.surf.gii rename to demos/fmap_failexemple/sub-55809/ses-Baseline/fmap/sub-55809_ses-Baseline_acq-se_dir-AP_run-1_epi.nii.gz diff --git a/demos/fmap_failexemple/sub-55809/ses-Baseline/fmap/sub-55809_ses-Baseline_acq-se_dir-PA_run-1_epi.json b/demos/fmap_failexemple/sub-55809/ses-Baseline/fmap/sub-55809_ses-Baseline_acq-se_dir-PA_run-1_epi.json new file mode 100644 index 00000000..de0935b1 --- /dev/null +++ b/demos/fmap_failexemple/sub-55809/ses-Baseline/fmap/sub-55809_ses-Baseline_acq-se_dir-PA_run-1_epi.json @@ -0,0 +1,73 @@ +{ + "Modality": "MR", + "MagneticFieldStrength": 3, + "ImagingFrequency": 123.251, + "Manufacturer": "Siemens", + "ManufacturersModelName": "Prisma", + "InstitutionName": "Hospital", + "InstitutionalDepartmentName": "Department", + "InstitutionAddress": "Blegdamsvej 9,Copenhagen,District,DK,2100", + "DeviceSerialNumber": "166141", + "StationName": "AWP166141", + "BodyPartExamined": "BRAIN", + "PatientPosition": "HFS", + "ProcedureStepDescription": "NP2^Head", + "SoftwareVersions": "syngo MR E11", + "MRAcquisitionType": "2D", + "SeriesDescription": "SpinEchoFieldMap_PA", + "ProtocolName": "SpinEchoFieldMap_PA", + "ScanningSequence": "EP", + "SequenceVariant": "SK", + "ScanOptions": "FS", + "SequenceName": "epse2d1_104", + "ImageType": [ + "ORIGINAL", + "PRIMARY", + "M", + "ND", + "MOSAIC" + ], + "NonlinearGradientCorrection": false, + "SeriesNumber": 11, + "AcquisitionTime": "10:43:19.027500", + "AcquisitionNumber": 1, + "SliceThickness": 2, + "SpacingBetweenSlices": 2, + "SAR": 0.148123, + "EchoTime": 0.066, + "RepetitionTime": 8, + "FlipAngle": 90, + "PartialFourier": 1, + "BaseResolution": 104, + "ShimSetting": [55,-11471,8076,384,34,175,-6,-9], + "TxRefAmp": 240.18, + "PhaseResolution": 1, + "VendorReportedEchoSpacing": 0.00058, + "ReceiveCoilName": "Head_32", + "ReceiveCoilActiveElements": "HEA;HEP", + "PulseSequenceDetails": "%CustomerSeq%\\cmrr_mbep2d_se", + "WipMemBlock": "fe0f5b8e-254a-414d-86d4-0d6fc04393c4||Sequence: R016 ve11e/master r/434b28f1; Aug 7 2019 19:20:16 by eja", + "CoilCombinationMethod": "Sum of Squares", + "ConsistencyInfo": "N4_VE11E_LATEST_20181129", + "MatrixCoilMode": "SENSE", + "PercentPhaseFOV": 100, + "PercentSampling": 100, + "EchoTrainLength": 104, + "PhaseEncodingSteps": 104, + "AcquisitionMatrixPE": 104, + "ReconMatrixPE": 104, + "BandwidthPerPixelPhaseEncode": 16.578, + "EffectiveEchoSpacing": 0.000580009, + "DerivedVendorReportedEchoSpacing": 0.000580009, + "TotalReadoutTime": 0.0597409, + "PixelBandwidth": 2290, + "DwellTime": 2.1e-06, + "PhaseEncodingDirection": "j", + "SliceTiming": [3.99,0,4.1025,0.1125,4.2125,0.2225,4.3225,0.3325,4.435,0.445,4.545,0.555,4.655,0.665,4.7675,0.7775,4.8775,0.8875,4.9875,0.9975000000000001,5.1,1.11,5.21,1.22,5.32,1.33,5.4325,1.4425,5.5425,1.5525,5.6525,1.6625,5.765,1.775,5.875,1.885,5.985,1.995,6.0975,2.1075,6.2075,2.2175,6.3175,2.3275,6.43,2.44,6.54,2.55,6.65,2.66,6.7625,2.7725,6.8725,2.8825,6.9825,2.9925,7.095,3.105,7.205,3.215,7.315,3.325,7.4275,3.4375,7.5375,3.5475,7.6475,3.6575,7.76,3.77,7.87,3.88], + "ImageOrientationPatientDICOM": [0.999947,0.0102972,-4.68781e-05,-0.0102973,0.999937,-0.00455223], + "ImageOrientationText": "Tra>Cor(-0.3)", + "InPlanePhaseEncodingDirectionDICOM": "COL", + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20220720", + "generatedfrom": "SPINECHOFIELDMAP_PA_0011" +} diff --git a/tests/data/SurfaceData/sub-06_hemi-R_space-individual_den-native_thickness.shape.gii b/demos/fmap_failexemple/sub-55809/ses-Baseline/fmap/sub-55809_ses-Baseline_acq-se_dir-PA_run-1_epi.nii.gz similarity index 100% rename from tests/data/SurfaceData/sub-06_hemi-R_space-individual_den-native_thickness.shape.gii rename to demos/fmap_failexemple/sub-55809/ses-Baseline/fmap/sub-55809_ses-Baseline_acq-se_dir-PA_run-1_epi.nii.gz diff --git a/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-AP_run-1_magnitude1.json b/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-AP_run-1_magnitude1.json new file mode 100644 index 00000000..50531e73 --- /dev/null +++ b/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-AP_run-1_magnitude1.json @@ -0,0 +1,62 @@ +{ + "Modality": "MR", + "MagneticFieldStrength": 3, + "ImagingFrequency": 123.251, + "Manufacturer": "Siemens", + "ManufacturersModelName": "Prisma", + "InstitutionName": "Rigshospitalet", + "InstitutionalDepartmentName": "Department of Neurology", + "InstitutionAddress": "Blegdamsvej 9,Copenhagen,District,DK,2100", + "DeviceSerialNumber": "166141", + "StationName": "RHRTGSIEMR01", + "BodyPartExamined": "BRAIN", + "PatientPosition": "HFS", + "ProcedureStepDescription": "MR projekt - neuro", + "SoftwareVersions": "syngo MR E11", + "MRAcquisitionType": "2D", + "SeriesDescription": "gre_field_mapping_AP", + "ProtocolName": "gre_field_mapping_AP", + "ScanningSequence": "GR", + "SequenceVariant": "SP", + "SequenceName": "*fm2d2r", + "ImageType": [ + "ORIGINAL", + "PRIMARY", + "M", + "ND", + "NORM" + ], + "SeriesNumber": 16, + "AcquisitionTime": "17:43:57.840000", + "AcquisitionNumber": 1, + "SliceThickness": 3, + "SpacingBetweenSlices": 3, + "SAR": 0.114116, + "EchoNumber": 1, + "EchoTime": 0.00492, + "RepetitionTime": 0.768, + "FlipAngle": 60, + "PartialFourier": 1, + "BaseResolution": 76, + "ShimSetting": [70,-11474,8083,358,-23,305,0,1], + "TxRefAmp": 257.989, + "PhaseResolution": 1, + "ReceiveCoilName": "Head_32", + "ReceiveCoilActiveElements": "HEA;HEP", + "PulseSequenceDetails": "%SiemensSeq%\\gre_field_mapping", + "ConsistencyInfo": "N4_VE11E_LATEST_20181129", + "PercentPhaseFOV": 100, + "PercentSampling": 100, + "PhaseEncodingSteps": 76, + "AcquisitionMatrixPE": 76, + "ReconMatrixPE": 76, + "PixelBandwidth": 280, + "DwellTime": 2.34e-05, + "PhaseEncodingDirection": "j-", + "SliceTiming": [0.39062,0,0.39062,0.01562,0.40625,0.01562,0.40625,0.03125,0.42188,0.04688,0.4375,0.04688,0.45312,0.0625,0.45312,0.07811999999999999,0.46875,0.07811999999999999,0.48438,0.09375,0.48438,0.10938,0.5,0.10938,0.51562,0.125,0.51562,0.14062,0.53125,0.15625,0.54688,0.15625,0.54688,0.17188,0.5625,0.1875,0.57812,0.1875,0.59375,0.20312,0.59375,0.21875,0.60938,0.21875,0.60938,0.23438,0.625,0.25,0.64062,0.25,0.65625,0.26562,0.65625,0.28125,0.67188,0.28125,0.6875,0.29688,0.6875,0.3125,0.70312,0.3125,0.71875,0.32812,0.71875,0.34375,0.73438,0.34375,0.75,0.35938,0.75,0.375], + "ImageOrientationPatientDICOM": [0.999042,0.0406194,0.0162991,-0.0408242,0.999089,0.0124404], + "InPlanePhaseEncodingDirectionDICOM": "COL", + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20210317", + "generatedfrom": "GRE_FIELD_MAPPING_AP_0016" +} diff --git a/tests/data/SurfaceData/sub-06_space-individual_den-native_thickness.dscalar.nii b/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-AP_run-1_magnitude1.nii.gz similarity index 100% rename from tests/data/SurfaceData/sub-06_space-individual_den-native_thickness.dscalar.nii rename to demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-AP_run-1_magnitude1.nii.gz diff --git a/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-AP_run-1_magnitude2.json b/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-AP_run-1_magnitude2.json new file mode 100644 index 00000000..b223970c --- /dev/null +++ b/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-AP_run-1_magnitude2.json @@ -0,0 +1,62 @@ +{ + "Modality": "MR", + "MagneticFieldStrength": 3, + "ImagingFrequency": 123.251, + "Manufacturer": "Siemens", + "ManufacturersModelName": "Prisma", + "InstitutionName": "Rigshospitalet", + "InstitutionalDepartmentName": "Department of Neurology", + "InstitutionAddress": "Blegdamsvej 9,Copenhagen,District,DK,2100", + "DeviceSerialNumber": "166141", + "StationName": "RHRTGSIEMR01", + "BodyPartExamined": "BRAIN", + "PatientPosition": "HFS", + "ProcedureStepDescription": "MR projekt - neuro", + "SoftwareVersions": "syngo MR E11", + "MRAcquisitionType": "2D", + "SeriesDescription": "gre_field_mapping_AP", + "ProtocolName": "gre_field_mapping_AP", + "ScanningSequence": "GR", + "SequenceVariant": "SP", + "SequenceName": "*fm2d2r", + "ImageType": [ + "ORIGINAL", + "PRIMARY", + "M", + "ND", + "NORM" + ], + "SeriesNumber": 16, + "AcquisitionTime": "17:43:58.610000", + "AcquisitionNumber": 1, + "SliceThickness": 3, + "SpacingBetweenSlices": 3, + "SAR": 0.114116, + "EchoNumber": 2, + "EchoTime": 0.00738, + "RepetitionTime": 0.768, + "FlipAngle": 60, + "PartialFourier": 1, + "BaseResolution": 76, + "ShimSetting": [70,-11474,8083,358,-23,305,0,1], + "TxRefAmp": 257.989, + "PhaseResolution": 1, + "ReceiveCoilName": "Head_32", + "ReceiveCoilActiveElements": "HEA;HEP", + "PulseSequenceDetails": "%SiemensSeq%\\gre_field_mapping", + "ConsistencyInfo": "N4_VE11E_LATEST_20181129", + "PercentPhaseFOV": 100, + "PercentSampling": 100, + "PhaseEncodingSteps": 76, + "AcquisitionMatrixPE": 76, + "ReconMatrixPE": 76, + "PixelBandwidth": 280, + "DwellTime": 2.34e-05, + "PhaseEncodingDirection": "j-", + "SliceTiming": [0.39062,0,0.39062,0.01562,0.40625,0.03125,0.42188,0.03125,0.4375,0.04688,0.4375,0.0625,0.45312,0.0625,0.46875,0.07811999999999999,0.46875,0.09375,0.48438,0.09375,0.5,0.10938,0.5,0.125,0.51562,0.125,0.53125,0.14062,0.53125,0.15625,0.54688,0.15625,0.5625,0.17188,0.5625,0.1875,0.57812,0.20312,0.59375,0.20312,0.59375,0.21875,0.60938,0.23438,0.625,0.23438,0.64062,0.25,0.64062,0.26562,0.65625,0.26562,0.67188,0.28125,0.67188,0.29688,0.6875,0.29688,0.70312,0.3125,0.70312,0.32812,0.71875,0.32812,0.73438,0.34375,0.73438,0.35938,0.75,0.35938,0.76562,0.375], + "ImageOrientationPatientDICOM": [0.999042,0.0406194,0.0162991,-0.0408242,0.999089,0.0124404], + "InPlanePhaseEncodingDirectionDICOM": "COL", + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20210317", + "generatedfrom": "GRE_FIELD_MAPPING_AP_0016" +} diff --git a/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-AP_run-1_magnitude2.nii.gz b/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-AP_run-1_magnitude2.nii.gz new file mode 100644 index 00000000..e69de29b diff --git a/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-AP_run-1_phasediff.json b/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-AP_run-1_phasediff.json new file mode 100644 index 00000000..6f3ff0cf --- /dev/null +++ b/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-AP_run-1_phasediff.json @@ -0,0 +1,64 @@ +{ + "Modality": "MR", + "MagneticFieldStrength": 3, + "ImagingFrequency": 123.251, + "Manufacturer": "Siemens", + "ManufacturersModelName": "Prisma", + "InstitutionName": "Rigshospitalet", + "InstitutionalDepartmentName": "Department of Neurology", + "InstitutionAddress": "Blegdamsvej 9,Copenhagen,District,DK,2100", + "DeviceSerialNumber": "166141", + "StationName": "RHRTGSIEMR01", + "BodyPartExamined": "BRAIN", + "PatientPosition": "HFS", + "ProcedureStepDescription": "MR projekt - neuro", + "SoftwareVersions": "syngo MR E11", + "MRAcquisitionType": "2D", + "SeriesDescription": "gre_field_mapping_AP", + "ProtocolName": "gre_field_mapping_AP", + "ScanningSequence": "GR", + "SequenceVariant": "SP", + "SequenceName": "*fm2d2r", + "ImageType": [ + "ORIGINAL", + "PRIMARY", + "P", + "ND", + "PHASE" + ], + "SeriesNumber": 17, + "AcquisitionTime": "17:43:58.610000", + "AcquisitionNumber": 1, + "SliceThickness": 3, + "SpacingBetweenSlices": 3, + "SAR": 0.114116, + "EchoNumber": 2, + "EchoTime": 0.00738, + "RepetitionTime": 0.768, + "FlipAngle": 60, + "EchoTime1": 0.00492, + "EchoTime2": 0.00738, + "PartialFourier": 1, + "BaseResolution": 76, + "ShimSetting": [70,-11474,8083,358,-23,305,0,1], + "TxRefAmp": 257.989, + "PhaseResolution": 1, + "ReceiveCoilName": "Head_32", + "ReceiveCoilActiveElements": "HEA;HEP", + "PulseSequenceDetails": "%SiemensSeq%\\gre_field_mapping", + "ConsistencyInfo": "N4_VE11E_LATEST_20181129", + "PercentPhaseFOV": 100, + "PercentSampling": 100, + "PhaseEncodingSteps": 76, + "AcquisitionMatrixPE": 76, + "ReconMatrixPE": 76, + "PixelBandwidth": 280, + "DwellTime": 2.34e-05, + "PhaseEncodingDirection": "j-", + "SliceTiming": [0.39062,0,0.39062,0.01562,0.40625,0.03125,0.42188,0.03125,0.4375,0.04688,0.4375,0.0625,0.45312,0.0625,0.46875,0.07811999999999999,0.46875,0.09375,0.48438,0.09375,0.5,0.10938,0.5,0.125,0.51562,0.125,0.53125,0.14062,0.53125,0.15625,0.54688,0.15625,0.5625,0.17188,0.5625,0.1875,0.57812,0.20312,0.59375,0.20312,0.59375,0.21875,0.60938,0.23438,0.625,0.23438,0.64062,0.25,0.64062,0.26562,0.65625,0.26562,0.67188,0.28125,0.67188,0.29688,0.6875,0.29688,0.70312,0.3125,0.70312,0.32812,0.71875,0.32812,0.73438,0.34375,0.73438,0.35938,0.75,0.35938,0.76562,0.375], + "ImageOrientationPatientDICOM": [0.999042,0.0406194,0.0162991,-0.0408242,0.999089,0.0124404], + "InPlanePhaseEncodingDirectionDICOM": "COL", + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20210317", + "generatedfrom": "GRE_FIELD_MAPPING_AP_0017" +} diff --git a/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-AP_run-1_phasediff.nii.gz b/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-AP_run-1_phasediff.nii.gz new file mode 100644 index 00000000..e69de29b diff --git a/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-LR_run-1_magnitude1.json b/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-LR_run-1_magnitude1.json new file mode 100644 index 00000000..ddac02dc --- /dev/null +++ b/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-LR_run-1_magnitude1.json @@ -0,0 +1,62 @@ +{ + "Modality": "MR", + "MagneticFieldStrength": 3, + "ImagingFrequency": 123.251, + "Manufacturer": "Siemens", + "ManufacturersModelName": "Prisma", + "InstitutionName": "Rigshospitalet", + "InstitutionalDepartmentName": "Department of Neurology", + "InstitutionAddress": "Blegdamsvej 9,Copenhagen,District,DK,2100", + "DeviceSerialNumber": "166141", + "StationName": "RHRTGSIEMR01", + "BodyPartExamined": "BRAIN", + "PatientPosition": "HFS", + "ProcedureStepDescription": "MR projekt - neuro", + "SoftwareVersions": "syngo MR E11", + "MRAcquisitionType": "2D", + "SeriesDescription": "gre_field_mapping", + "ProtocolName": "gre_field_mapping", + "ScanningSequence": "GR", + "SequenceVariant": "SP", + "SequenceName": "*fm2d2r", + "ImageType": [ + "ORIGINAL", + "PRIMARY", + "M", + "ND", + "NORM" + ], + "SeriesNumber": 13, + "AcquisitionTime": "17:41:57.937500", + "AcquisitionNumber": 1, + "SliceThickness": 3, + "SpacingBetweenSlices": 3, + "SAR": 0.114706, + "EchoNumber": 1, + "EchoTime": 0.00492, + "RepetitionTime": 0.764, + "FlipAngle": 60, + "PartialFourier": 1, + "BaseResolution": 76, + "ShimSetting": [70,-11474,8083,358,-23,305,0,1], + "TxRefAmp": 257.989, + "PhaseResolution": 1, + "ReceiveCoilName": "Head_32", + "ReceiveCoilActiveElements": "HEA;HEP", + "PulseSequenceDetails": "%SiemensSeq%\\gre_field_mapping", + "ConsistencyInfo": "N4_VE11E_LATEST_20181129", + "PercentPhaseFOV": 100, + "PercentSampling": 100, + "PhaseEncodingSteps": 76, + "AcquisitionMatrixPE": 76, + "ReconMatrixPE": 76, + "PixelBandwidth": 290, + "DwellTime": 2.27e-05, + "PhaseEncodingDirection": "i", + "SliceTiming": [0.39062,0,0.39062,0.01562,0.40625,0.03125,0.42188,0.03125,0.42188,0.04688,0.4375,0.0625,0.45312,0.0625,0.45312,0.07811999999999999,0.46875,0.09375,0.48438,0.09375,0.48438,0.10938,0.5,0.125,0.51562,0.125,0.51562,0.14062,0.53125,0.15625,0.54688,0.15625,0.5625,0.17188,0.5625,0.1875,0.57812,0.1875,0.59375,0.20312,0.59375,0.21875,0.60938,0.21875,0.625,0.23438,0.625,0.25,0.64062,0.25,0.65625,0.26562,0.65625,0.28125,0.67188,0.29688,0.6875,0.29688,0.6875,0.3125,0.70312,0.32812,0.71875,0.32812,0.71875,0.34375,0.73438,0.35938,0.75,0.35938,0.75,0.375], + "ImageOrientationPatientDICOM": [0.999042,0.0406194,0.0162991,-0.0408243,0.999089,0.0124404], + "InPlanePhaseEncodingDirectionDICOM": "ROW", + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20210317", + "generatedfrom": "GRE_FIELD_MAPPING_0013" +} diff --git a/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-LR_run-1_magnitude1.nii.gz b/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-LR_run-1_magnitude1.nii.gz new file mode 100644 index 00000000..e69de29b diff --git a/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-LR_run-1_magnitude2.json b/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-LR_run-1_magnitude2.json new file mode 100644 index 00000000..d85fd01a --- /dev/null +++ b/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-LR_run-1_magnitude2.json @@ -0,0 +1,62 @@ +{ + "Modality": "MR", + "MagneticFieldStrength": 3, + "ImagingFrequency": 123.251, + "Manufacturer": "Siemens", + "ManufacturersModelName": "Prisma", + "InstitutionName": "Rigshospitalet", + "InstitutionalDepartmentName": "Department of Neurology", + "InstitutionAddress": "Blegdamsvej 9,Copenhagen,District,DK,2100", + "DeviceSerialNumber": "166141", + "StationName": "RHRTGSIEMR01", + "BodyPartExamined": "BRAIN", + "PatientPosition": "HFS", + "ProcedureStepDescription": "MR projekt - neuro", + "SoftwareVersions": "syngo MR E11", + "MRAcquisitionType": "2D", + "SeriesDescription": "gre_field_mapping", + "ProtocolName": "gre_field_mapping", + "ScanningSequence": "GR", + "SequenceVariant": "SP", + "SequenceName": "*fm2d2r", + "ImageType": [ + "ORIGINAL", + "PRIMARY", + "M", + "ND", + "NORM" + ], + "SeriesNumber": 13, + "AcquisitionTime": "17:41:58.702500", + "AcquisitionNumber": 1, + "SliceThickness": 3, + "SpacingBetweenSlices": 3, + "SAR": 0.114706, + "EchoNumber": 2, + "EchoTime": 0.00738, + "RepetitionTime": 0.764, + "FlipAngle": 60, + "PartialFourier": 1, + "BaseResolution": 76, + "ShimSetting": [70,-11474,8083,358,-23,305,0,1], + "TxRefAmp": 257.989, + "PhaseResolution": 1, + "ReceiveCoilName": "Head_32", + "ReceiveCoilActiveElements": "HEA;HEP", + "PulseSequenceDetails": "%SiemensSeq%\\gre_field_mapping", + "ConsistencyInfo": "N4_VE11E_LATEST_20181129", + "PercentPhaseFOV": 100, + "PercentSampling": 100, + "PhaseEncodingSteps": 76, + "AcquisitionMatrixPE": 76, + "ReconMatrixPE": 76, + "PixelBandwidth": 290, + "DwellTime": 2.27e-05, + "PhaseEncodingDirection": "i", + "SliceTiming": [0.39062,0,0.39062,0.01562,0.40625,0.03125,0.42188,0.03125,0.42188,0.04688,0.4375,0.0625,0.45312,0.0625,0.45312,0.07811999999999999,0.46875,0.09375,0.48438,0.09375,0.48438,0.10938,0.5,0.125,0.51562,0.125,0.53125,0.14062,0.53125,0.15625,0.54688,0.15625,0.5625,0.17188,0.5625,0.1875,0.57812,0.1875,0.59375,0.20312,0.59375,0.21875,0.60938,0.23438,0.625,0.23438,0.625,0.25,0.64062,0.26562,0.65625,0.26562,0.65625,0.28125,0.67188,0.29688,0.6875,0.29688,0.6875,0.3125,0.70312,0.32812,0.71875,0.32812,0.71875,0.34375,0.73438,0.35938,0.75,0.35938,0.75,0.375], + "ImageOrientationPatientDICOM": [0.999042,0.0406194,0.0162991,-0.0408243,0.999089,0.0124404], + "InPlanePhaseEncodingDirectionDICOM": "ROW", + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20210317", + "generatedfrom": "GRE_FIELD_MAPPING_0013" +} diff --git a/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-LR_run-1_magnitude2.nii.gz b/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-LR_run-1_magnitude2.nii.gz new file mode 100644 index 00000000..e69de29b diff --git a/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-LR_run-1_phasediff.json b/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-LR_run-1_phasediff.json new file mode 100644 index 00000000..96f70745 --- /dev/null +++ b/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-LR_run-1_phasediff.json @@ -0,0 +1,64 @@ +{ + "Modality": "MR", + "MagneticFieldStrength": 3, + "ImagingFrequency": 123.251, + "Manufacturer": "Siemens", + "ManufacturersModelName": "Prisma", + "InstitutionName": "Rigshospitalet", + "InstitutionalDepartmentName": "Department of Neurology", + "InstitutionAddress": "Blegdamsvej 9,Copenhagen,District,DK,2100", + "DeviceSerialNumber": "166141", + "StationName": "RHRTGSIEMR01", + "BodyPartExamined": "BRAIN", + "PatientPosition": "HFS", + "ProcedureStepDescription": "MR projekt - neuro", + "SoftwareVersions": "syngo MR E11", + "MRAcquisitionType": "2D", + "SeriesDescription": "gre_field_mapping", + "ProtocolName": "gre_field_mapping", + "ScanningSequence": "GR", + "SequenceVariant": "SP", + "SequenceName": "*fm2d2r", + "ImageType": [ + "ORIGINAL", + "PRIMARY", + "P", + "ND", + "PHASE" + ], + "SeriesNumber": 14, + "AcquisitionTime": "17:41:58.702500", + "AcquisitionNumber": 1, + "SliceThickness": 3, + "SpacingBetweenSlices": 3, + "SAR": 0.114706, + "EchoNumber": 2, + "EchoTime": 0.00738, + "RepetitionTime": 0.764, + "FlipAngle": 60, + "EchoTime1": 0.00492, + "EchoTime2": 0.00738, + "PartialFourier": 1, + "BaseResolution": 76, + "ShimSetting": [70,-11474,8083,358,-23,305,0,1], + "TxRefAmp": 257.989, + "PhaseResolution": 1, + "ReceiveCoilName": "Head_32", + "ReceiveCoilActiveElements": "HEA;HEP", + "PulseSequenceDetails": "%SiemensSeq%\\gre_field_mapping", + "ConsistencyInfo": "N4_VE11E_LATEST_20181129", + "PercentPhaseFOV": 100, + "PercentSampling": 100, + "PhaseEncodingSteps": 76, + "AcquisitionMatrixPE": 76, + "ReconMatrixPE": 76, + "PixelBandwidth": 290, + "DwellTime": 2.27e-05, + "PhaseEncodingDirection": "i", + "SliceTiming": [0.39062,0,0.39062,0.01562,0.40625,0.03125,0.42188,0.03125,0.42188,0.04688,0.4375,0.0625,0.45312,0.0625,0.45312,0.07811999999999999,0.46875,0.09375,0.48438,0.09375,0.48438,0.10938,0.5,0.125,0.51562,0.125,0.53125,0.14062,0.53125,0.15625,0.54688,0.15625,0.5625,0.17188,0.5625,0.1875,0.57812,0.1875,0.59375,0.20312,0.59375,0.21875,0.60938,0.23438,0.625,0.23438,0.625,0.25,0.64062,0.26562,0.65625,0.26562,0.65625,0.28125,0.67188,0.29688,0.6875,0.29688,0.6875,0.3125,0.70312,0.32812,0.71875,0.32812,0.71875,0.34375,0.73438,0.35938,0.75,0.35938,0.75,0.375], + "ImageOrientationPatientDICOM": [0.999042,0.0406194,0.0162991,-0.0408243,0.999089,0.0124404], + "InPlanePhaseEncodingDirectionDICOM": "ROW", + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20210317", + "generatedfrom": "GRE_FIELD_MAPPING_0014" +} diff --git a/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-LR_run-1_phasediff.nii.gz b/demos/fmap_failexemple/sub-57460/ses-1wkfollowup/fmap/sub-57460_ses-1wkfollowup_acq-gre_dir-LR_run-1_phasediff.nii.gz new file mode 100644 index 00000000..e69de29b diff --git a/demos/fmap_failexemple/sub-57460/ses-Baseline/fmap/sub-57460_ses-Baseline_acq-se_dir-AP_run-1_epi.json b/demos/fmap_failexemple/sub-57460/ses-Baseline/fmap/sub-57460_ses-Baseline_acq-se_dir-AP_run-1_epi.json new file mode 100644 index 00000000..176814da --- /dev/null +++ b/demos/fmap_failexemple/sub-57460/ses-Baseline/fmap/sub-57460_ses-Baseline_acq-se_dir-AP_run-1_epi.json @@ -0,0 +1,69 @@ +{ + "Modality": "MR", + "MagneticFieldStrength": 3, + "ImagingFrequency": 123.251, + "Manufacturer": "Siemens", + "ManufacturersModelName": "Prisma", + "InstitutionName": "Rigshospitalet", + "InstitutionalDepartmentName": "Department of Neurology", + "InstitutionAddress": "Blegdamsvej 9,Copenhagen,District,DK,2100", + "DeviceSerialNumber": "166141", + "StationName": "RHRTGSIEMR01", + "BodyPartExamined": "BRAIN", + "PatientPosition": "HFS", + "ProcedureStepDescription": "MR projekt - neuro", + "SoftwareVersions": "syngo MR E11", + "MRAcquisitionType": "2D", + "SeriesDescription": "SpinEchoFieldMap_AP", + "ProtocolName": "SpinEchoFieldMap_AP", + "ScanningSequence": "EP", + "SequenceVariant": "SK", + "ScanOptions": "FS", + "SequenceName": "epse2d1_104", + "ImageType": [ + "ORIGINAL", + "PRIMARY", + "M", + "ND", + "MOSAIC" + ], + "SeriesNumber": 8, + "AcquisitionTime": "10:56:13.677500", + "AcquisitionNumber": 1, + "SliceThickness": 2, + "SpacingBetweenSlices": 2, + "SAR": 0.160826, + "EchoTime": 0.066, + "RepetitionTime": 8, + "FlipAngle": 90, + "PartialFourier": 1, + "BaseResolution": 104, + "ShimSetting": [48,-11456,8095,367,-50,293,-21,-4], + "TxRefAmp": 261.463, + "PhaseResolution": 1, + "VendorReportedEchoSpacing": 0.00058, + "ReceiveCoilName": "Head_32", + "ReceiveCoilActiveElements": "HEA;HEP", + "PulseSequenceDetails": "%CustomerSeq%\\cmrr_mbep2d_se", + "WipMemBlock": "9b25abd5-063b-4e07-89d1-9f1c70f7f5bf||Sequence: R016 ve11e/master r/434b28f1; Aug 7 2019 19:20:16 by eja", + "ConsistencyInfo": "N4_VE11E_LATEST_20181129", + "PercentPhaseFOV": 100, + "PercentSampling": 100, + "EchoTrainLength": 104, + "PhaseEncodingSteps": 104, + "AcquisitionMatrixPE": 104, + "ReconMatrixPE": 104, + "BandwidthPerPixelPhaseEncode": 16.578, + "EffectiveEchoSpacing": 0.000580009, + "DerivedVendorReportedEchoSpacing": 0.000580009, + "TotalReadoutTime": 0.0597409, + "PixelBandwidth": 2290, + "DwellTime": 2.1e-06, + "PhaseEncodingDirection": "j-", + "SliceTiming": [3.99,0,4.1,0.11,4.2125,0.2225,4.3225,0.3325,4.4325,0.4425,4.545,0.555,4.655,0.665,4.765,0.775,4.8775,0.8875,4.9875,0.9975000000000001,5.0975,1.1075,5.21,1.22,5.32,1.33,5.43,1.44,5.5425,1.5525,5.6525,1.6625,5.7625,1.7725,5.875,1.885,5.985,1.995,6.095,2.105,6.2075,2.2175,6.3175,2.3275,6.4275,2.4375,6.54,2.55,6.65,2.66,6.76,2.77,6.8725,2.8825,6.9825,2.9925,7.0925,3.1025,7.205,3.215,7.315,3.325,7.425,3.435,7.5375,3.5475,7.6475,3.6575,7.7575,3.7675,7.87,3.88], + "ImageOrientationPatientDICOM": [0.999143,0.0325644,-0.0255376,-0.0322722,0.99941,0.0117744], + "InPlanePhaseEncodingDirectionDICOM": "COL", + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20210317", + "generatedfrom": "SPINECHOFIELDMAP_AP_0008" +} diff --git a/demos/fmap_failexemple/sub-57460/ses-Baseline/fmap/sub-57460_ses-Baseline_acq-se_dir-AP_run-1_epi.nii.gz b/demos/fmap_failexemple/sub-57460/ses-Baseline/fmap/sub-57460_ses-Baseline_acq-se_dir-AP_run-1_epi.nii.gz new file mode 100644 index 00000000..e69de29b diff --git a/demos/fmap_failexemple/sub-57460/ses-Baseline/fmap/sub-57460_ses-Baseline_acq-se_dir-AP_run-2_epi.json b/demos/fmap_failexemple/sub-57460/ses-Baseline/fmap/sub-57460_ses-Baseline_acq-se_dir-AP_run-2_epi.json new file mode 100644 index 00000000..1cccc1c7 --- /dev/null +++ b/demos/fmap_failexemple/sub-57460/ses-Baseline/fmap/sub-57460_ses-Baseline_acq-se_dir-AP_run-2_epi.json @@ -0,0 +1,69 @@ +{ + "Modality": "MR", + "MagneticFieldStrength": 3, + "ImagingFrequency": 123.251, + "Manufacturer": "Siemens", + "ManufacturersModelName": "Prisma", + "InstitutionName": "Rigshospitalet", + "InstitutionalDepartmentName": "Department of Neurology", + "InstitutionAddress": "Blegdamsvej 9,Copenhagen,District,DK,2100", + "DeviceSerialNumber": "166141", + "StationName": "RHRTGSIEMR01", + "BodyPartExamined": "BRAIN", + "PatientPosition": "HFS", + "ProcedureStepDescription": "MR projekt - neuro", + "SoftwareVersions": "syngo MR E11", + "MRAcquisitionType": "2D", + "SeriesDescription": "SpinEchoFieldMap_AP", + "ProtocolName": "SpinEchoFieldMap_AP", + "ScanningSequence": "EP", + "SequenceVariant": "SK", + "ScanOptions": "FS", + "SequenceName": "epse2d1_104", + "ImageType": [ + "ORIGINAL", + "PRIMARY", + "M", + "ND", + "MOSAIC" + ], + "SeriesNumber": 26, + "AcquisitionTime": "11:39:51.580000", + "AcquisitionNumber": 1, + "SliceThickness": 2, + "SpacingBetweenSlices": 2, + "SAR": 0.160159, + "EchoTime": 0.066, + "RepetitionTime": 8, + "FlipAngle": 90, + "PartialFourier": 1, + "BaseResolution": 104, + "ShimSetting": [62,-11453,8106,336,-26,331,4,5], + "TxRefAmp": 259.529, + "PhaseResolution": 1, + "VendorReportedEchoSpacing": 0.00058, + "ReceiveCoilName": "Head_32", + "ReceiveCoilActiveElements": "HEA;HEP", + "PulseSequenceDetails": "%CustomerSeq%\\cmrr_mbep2d_se", + "WipMemBlock": "0f6d3ad5-d0e5-4e55-8c92-fd428ec9b1b2||Sequence: R016 ve11e/master r/434b28f1; Aug 7 2019 19:20:16 by eja", + "ConsistencyInfo": "N4_VE11E_LATEST_20181129", + "PercentPhaseFOV": 100, + "PercentSampling": 100, + "EchoTrainLength": 104, + "PhaseEncodingSteps": 104, + "AcquisitionMatrixPE": 104, + "ReconMatrixPE": 104, + "BandwidthPerPixelPhaseEncode": 16.578, + "EffectiveEchoSpacing": 0.000580009, + "DerivedVendorReportedEchoSpacing": 0.000580009, + "TotalReadoutTime": 0.0597409, + "PixelBandwidth": 2290, + "DwellTime": 2.1e-06, + "PhaseEncodingDirection": "j-", + "SliceTiming": [3.99,0,4.1,0.11,4.21,0.22,4.3225,0.3325,4.4325,0.4425,4.5425,0.5525,4.655,0.665,4.765,0.775,4.875,0.885,4.9875,0.9975000000000001,5.0975,1.1075,5.2075,1.2175,5.32,1.33,5.43,1.44,5.54,1.55,5.6525,1.6625,5.7625,1.7725,5.8725,1.8825,5.985,1.995,6.095,2.105,6.205,2.215,6.3175,2.3275,6.4275,2.4375,6.5375,2.5475,6.65,2.66,6.76,2.77,6.87,2.88,6.9825,2.9925,7.0925,3.1025,7.2025,3.2125,7.315,3.325,7.425,3.435,7.535,3.545,7.6475,3.6575,7.7575,3.7675,7.8675,3.8775], + "ImageOrientationPatientDICOM": [0.999645,0.0162352,0.0211344,-0.0162273,0.999868,-0.000547872], + "InPlanePhaseEncodingDirectionDICOM": "COL", + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20210317", + "generatedfrom": "SPINECHOFIELDMAP_AP_0026" +} diff --git a/demos/fmap_failexemple/sub-57460/ses-Baseline/fmap/sub-57460_ses-Baseline_acq-se_dir-AP_run-2_epi.nii.gz b/demos/fmap_failexemple/sub-57460/ses-Baseline/fmap/sub-57460_ses-Baseline_acq-se_dir-AP_run-2_epi.nii.gz new file mode 100644 index 00000000..e69de29b diff --git a/demos/fmap_failexemple/sub-57460/ses-Baseline/fmap/sub-57460_ses-Baseline_acq-se_dir-PA_run-1_epi.json b/demos/fmap_failexemple/sub-57460/ses-Baseline/fmap/sub-57460_ses-Baseline_acq-se_dir-PA_run-1_epi.json new file mode 100644 index 00000000..061a50db --- /dev/null +++ b/demos/fmap_failexemple/sub-57460/ses-Baseline/fmap/sub-57460_ses-Baseline_acq-se_dir-PA_run-1_epi.json @@ -0,0 +1,69 @@ +{ + "Modality": "MR", + "MagneticFieldStrength": 3, + "ImagingFrequency": 123.251, + "Manufacturer": "Siemens", + "ManufacturersModelName": "Prisma", + "InstitutionName": "Rigshospitalet", + "InstitutionalDepartmentName": "Department of Neurology", + "InstitutionAddress": "Blegdamsvej 9,Copenhagen,District,DK,2100", + "DeviceSerialNumber": "166141", + "StationName": "RHRTGSIEMR01", + "BodyPartExamined": "BRAIN", + "PatientPosition": "HFS", + "ProcedureStepDescription": "MR projekt - neuro", + "SoftwareVersions": "syngo MR E11", + "MRAcquisitionType": "2D", + "SeriesDescription": "SpinEchoFieldMap_PA", + "ProtocolName": "SpinEchoFieldMap_PA", + "ScanningSequence": "EP", + "SequenceVariant": "SK", + "ScanOptions": "FS", + "SequenceName": "epse2d1_104", + "ImageType": [ + "ORIGINAL", + "PRIMARY", + "M", + "ND", + "MOSAIC" + ], + "SeriesNumber": 10, + "AcquisitionTime": "10:56:28.432500", + "AcquisitionNumber": 1, + "SliceThickness": 2, + "SpacingBetweenSlices": 2, + "SAR": 0.160826, + "EchoTime": 0.066, + "RepetitionTime": 8, + "FlipAngle": 90, + "PartialFourier": 1, + "BaseResolution": 104, + "ShimSetting": [48,-11456,8095,367,-50,293,-21,-4], + "TxRefAmp": 261.463, + "PhaseResolution": 1, + "VendorReportedEchoSpacing": 0.00058, + "ReceiveCoilName": "Head_32", + "ReceiveCoilActiveElements": "HEA;HEP", + "PulseSequenceDetails": "%CustomerSeq%\\cmrr_mbep2d_se", + "WipMemBlock": "31567dce-2c21-4824-9473-1e773058ca01||Sequence: R016 ve11e/master r/434b28f1; Aug 7 2019 19:20:16 by eja", + "ConsistencyInfo": "N4_VE11E_LATEST_20181129", + "PercentPhaseFOV": 100, + "PercentSampling": 100, + "EchoTrainLength": 104, + "PhaseEncodingSteps": 104, + "AcquisitionMatrixPE": 104, + "ReconMatrixPE": 104, + "BandwidthPerPixelPhaseEncode": 16.578, + "EffectiveEchoSpacing": 0.000580009, + "DerivedVendorReportedEchoSpacing": 0.000580009, + "TotalReadoutTime": 0.0597409, + "PixelBandwidth": 2290, + "DwellTime": 2.1e-06, + "PhaseEncodingDirection": "j", + "SliceTiming": [3.99,0,4.1,0.1125,4.2125,0.2225,4.3225,0.3325,4.4325,0.445,4.545,0.555,4.655,0.665,4.765,0.7775,4.8775,0.8875,4.9875,0.9975000000000001,5.0975,1.11,5.21,1.22,5.32,1.33,5.43,1.4425,5.5425,1.5525,5.6525,1.6625,5.7625,1.775,5.875,1.885,5.985,1.995,6.095,2.1075,6.2075,2.2175,6.3175,2.3275,6.4275,2.44,6.54,2.55,6.65,2.66,6.76,2.7725,6.8725,2.8825,6.9825,2.9925,7.0925,3.105,7.205,3.215,7.315,3.325,7.425,3.435,7.5375,3.5475,7.6475,3.6575,7.7575,3.7675,7.87,3.88], + "ImageOrientationPatientDICOM": [0.999143,0.0325644,-0.0255376,-0.0322722,0.99941,0.0117744], + "InPlanePhaseEncodingDirectionDICOM": "COL", + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20210317", + "generatedfrom": "SPINECHOFIELDMAP_PA_0010" +} diff --git a/demos/fmap_failexemple/sub-57460/ses-Baseline/fmap/sub-57460_ses-Baseline_acq-se_dir-PA_run-1_epi.nii.gz b/demos/fmap_failexemple/sub-57460/ses-Baseline/fmap/sub-57460_ses-Baseline_acq-se_dir-PA_run-1_epi.nii.gz new file mode 100644 index 00000000..e69de29b diff --git a/demos/fmap_failexemple/sub-57460/ses-Baseline/fmap/sub-57460_ses-Baseline_acq-se_dir-PA_run-2_epi.json b/demos/fmap_failexemple/sub-57460/ses-Baseline/fmap/sub-57460_ses-Baseline_acq-se_dir-PA_run-2_epi.json new file mode 100644 index 00000000..fa230d88 --- /dev/null +++ b/demos/fmap_failexemple/sub-57460/ses-Baseline/fmap/sub-57460_ses-Baseline_acq-se_dir-PA_run-2_epi.json @@ -0,0 +1,69 @@ +{ + "Modality": "MR", + "MagneticFieldStrength": 3, + "ImagingFrequency": 123.251, + "Manufacturer": "Siemens", + "ManufacturersModelName": "Prisma", + "InstitutionName": "Rigshospitalet", + "InstitutionalDepartmentName": "Department of Neurology", + "InstitutionAddress": "Blegdamsvej 9,Copenhagen,District,DK,2100", + "DeviceSerialNumber": "166141", + "StationName": "RHRTGSIEMR01", + "BodyPartExamined": "BRAIN", + "PatientPosition": "HFS", + "ProcedureStepDescription": "MR projekt - neuro", + "SoftwareVersions": "syngo MR E11", + "MRAcquisitionType": "2D", + "SeriesDescription": "SpinEchoFieldMap_PA", + "ProtocolName": "SpinEchoFieldMap_PA", + "ScanningSequence": "EP", + "SequenceVariant": "SK", + "ScanOptions": "FS", + "SequenceName": "epse2d1_104", + "ImageType": [ + "ORIGINAL", + "PRIMARY", + "M", + "ND", + "MOSAIC" + ], + "SeriesNumber": 28, + "AcquisitionTime": "11:40:3.420000", + "AcquisitionNumber": 1, + "SliceThickness": 2, + "SpacingBetweenSlices": 2, + "SAR": 0.160159, + "EchoTime": 0.066, + "RepetitionTime": 8, + "FlipAngle": 90, + "PartialFourier": 1, + "BaseResolution": 104, + "ShimSetting": [62,-11453,8106,336,-26,331,4,5], + "TxRefAmp": 259.529, + "PhaseResolution": 1, + "VendorReportedEchoSpacing": 0.00058, + "ReceiveCoilName": "Head_32", + "ReceiveCoilActiveElements": "HEA;HEP", + "PulseSequenceDetails": "%CustomerSeq%\\cmrr_mbep2d_se", + "WipMemBlock": "eed3c12b-fa76-4eb5-a3ce-c46a9bb63740||Sequence: R016 ve11e/master r/434b28f1; Aug 7 2019 19:20:16 by eja", + "ConsistencyInfo": "N4_VE11E_LATEST_20181129", + "PercentPhaseFOV": 100, + "PercentSampling": 100, + "EchoTrainLength": 104, + "PhaseEncodingSteps": 104, + "AcquisitionMatrixPE": 104, + "ReconMatrixPE": 104, + "BandwidthPerPixelPhaseEncode": 16.578, + "EffectiveEchoSpacing": 0.000580009, + "DerivedVendorReportedEchoSpacing": 0.000580009, + "TotalReadoutTime": 0.0597409, + "PixelBandwidth": 2290, + "DwellTime": 2.1e-06, + "PhaseEncodingDirection": "j", + "SliceTiming": [3.99,0,4.1025,0.1125,4.2125,0.2225,4.3225,0.3325,4.435,0.445,4.545,0.555,4.655,0.665,4.7675,0.7775,4.8775,0.8875,4.9875,0.9975000000000001,5.1,1.11,5.21,1.22,5.32,1.33,5.4325,1.4425,5.5425,1.5525,5.6525,1.6625,5.765,1.775,5.875,1.885,5.985,1.995,6.0975,2.1075,6.2075,2.2175,6.3175,2.3275,6.43,2.44,6.54,2.55,6.65,2.66,6.7625,2.7725,6.8725,2.8825,6.9825,2.9925,7.095,3.105,7.205,3.215,7.315,3.325,7.4275,3.4375,7.5375,3.5475,7.6475,3.6575,7.76,3.77,7.87,3.88], + "ImageOrientationPatientDICOM": [0.999645,0.0162352,0.0211344,-0.0162273,0.999868,-0.000547872], + "InPlanePhaseEncodingDirectionDICOM": "COL", + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20210317", + "generatedfrom": "SPINECHOFIELDMAP_PA_0028" +} diff --git a/demos/fmap_failexemple/sub-57460/ses-Baseline/fmap/sub-57460_ses-Baseline_acq-se_dir-PA_run-2_epi.nii.gz b/demos/fmap_failexemple/sub-57460/ses-Baseline/fmap/sub-57460_ses-Baseline_acq-se_dir-PA_run-2_epi.nii.gz new file mode 100644 index 00000000..e69de29b diff --git a/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-AP_run-1_magnitude1.json b/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-AP_run-1_magnitude1.json new file mode 100644 index 00000000..64020348 --- /dev/null +++ b/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-AP_run-1_magnitude1.json @@ -0,0 +1,62 @@ +{ + "Modality": "MR", + "MagneticFieldStrength": 3, + "ImagingFrequency": 123.251, + "Manufacturer": "Siemens", + "ManufacturersModelName": "Prisma", + "InstitutionName": "Rigshospitalet", + "InstitutionalDepartmentName": "Department of Neurology", + "InstitutionAddress": "Blegdamsvej 9,Copenhagen,District,DK,2100", + "DeviceSerialNumber": "166141", + "StationName": "RHRTGSIEMR01", + "BodyPartExamined": "BRAIN", + "PatientPosition": "HFS", + "ProcedureStepDescription": "MR projekt - neuro", + "SoftwareVersions": "syngo MR E11", + "MRAcquisitionType": "2D", + "SeriesDescription": "gre_field_mapping_AP", + "ProtocolName": "gre_field_mapping_AP", + "ScanningSequence": "GR", + "SequenceVariant": "SP", + "SequenceName": "*fm2d2r", + "ImageType": [ + "ORIGINAL", + "PRIMARY", + "M", + "ND", + "NORM" + ], + "SeriesNumber": 16, + "AcquisitionTime": "09:23:38.867500", + "AcquisitionNumber": 1, + "SliceThickness": 3, + "SpacingBetweenSlices": 3.6, + "SAR": 0.08852649999999999, + "EchoNumber": 1, + "EchoTime": 0.00492, + "RepetitionTime": 0.4, + "FlipAngle": 60, + "PartialFourier": 1, + "BaseResolution": 76, + "ShimSetting": [62,-11422,7976,437,-29,369,-92,-16], + "TxRefAmp": 252.164, + "PhaseResolution": 1, + "ReceiveCoilName": "Head_32", + "ReceiveCoilActiveElements": "HEA;HEP", + "PulseSequenceDetails": "%SiemensSeq%\\gre_field_mapping", + "ConsistencyInfo": "N4_VE11E_LATEST_20181129", + "PercentPhaseFOV": 100, + "PercentSampling": 100, + "PhaseEncodingSteps": 76, + "AcquisitionMatrixPE": 76, + "ReconMatrixPE": 76, + "PixelBandwidth": 280, + "DwellTime": 2.34e-05, + "PhaseEncodingDirection": "j-", + "SliceTiming": [0,0.20312,0.01562,0.21875,0.02344,0.22656,0.03906,0.24219,0.04688,0.25,0.05469,0.26562,0.07031,0.27344,0.07811999999999999,0.28906,0.09375,0.29688,0.10156,0.30469,0.11719,0.32031,0.125,0.32812,0.14062,0.34375,0.14844,0.35938,0.16406,0.36719,0.17188,0.375,0.17969,0.39062,0.19531], + "ImageOrientationPatientDICOM": [0.999294,0.0318495,-0.0199537,-0.0362931,0.955685,-0.292144], + "InPlanePhaseEncodingDirectionDICOM": "COL", + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20210317", + "generatedfrom": "GRE_FIELD_MAPPING_AP_0016" +} diff --git a/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-AP_run-1_magnitude1.nii.gz b/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-AP_run-1_magnitude1.nii.gz new file mode 100644 index 00000000..e69de29b diff --git a/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-AP_run-1_magnitude2.json b/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-AP_run-1_magnitude2.json new file mode 100644 index 00000000..a041a129 --- /dev/null +++ b/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-AP_run-1_magnitude2.json @@ -0,0 +1,62 @@ +{ + "Modality": "MR", + "MagneticFieldStrength": 3, + "ImagingFrequency": 123.251, + "Manufacturer": "Siemens", + "ManufacturersModelName": "Prisma", + "InstitutionName": "Rigshospitalet", + "InstitutionalDepartmentName": "Department of Neurology", + "InstitutionAddress": "Blegdamsvej 9,Copenhagen,District,DK,2100", + "DeviceSerialNumber": "166141", + "StationName": "RHRTGSIEMR01", + "BodyPartExamined": "BRAIN", + "PatientPosition": "HFS", + "ProcedureStepDescription": "MR projekt - neuro", + "SoftwareVersions": "syngo MR E11", + "MRAcquisitionType": "2D", + "SeriesDescription": "gre_field_mapping_AP", + "ProtocolName": "gre_field_mapping_AP", + "ScanningSequence": "GR", + "SequenceVariant": "SP", + "SequenceName": "*fm2d2r", + "ImageType": [ + "ORIGINAL", + "PRIMARY", + "M", + "ND", + "NORM" + ], + "SeriesNumber": 16, + "AcquisitionTime": "09:23:39.270000", + "AcquisitionNumber": 1, + "SliceThickness": 3, + "SpacingBetweenSlices": 3.6, + "SAR": 0.08852649999999999, + "EchoNumber": 2, + "EchoTime": 0.00738, + "RepetitionTime": 0.4, + "FlipAngle": 60, + "PartialFourier": 1, + "BaseResolution": 76, + "ShimSetting": [62,-11422,7976,437,-29,369,-92,-16], + "TxRefAmp": 252.164, + "PhaseResolution": 1, + "ReceiveCoilName": "Head_32", + "ReceiveCoilActiveElements": "HEA;HEP", + "PulseSequenceDetails": "%SiemensSeq%\\gre_field_mapping", + "ConsistencyInfo": "N4_VE11E_LATEST_20181129", + "PercentPhaseFOV": 100, + "PercentSampling": 100, + "PhaseEncodingSteps": 76, + "AcquisitionMatrixPE": 76, + "ReconMatrixPE": 76, + "PixelBandwidth": 280, + "DwellTime": 2.34e-05, + "PhaseEncodingDirection": "j-", + "SliceTiming": [0,0.20312,0.00781,0.21094,0.01562,0.22656,0.03125,0.23438,0.03906,0.25,0.05469,0.25781,0.0625,0.27344,0.07811999999999999,0.28125,0.08594,0.29688,0.10156,0.30469,0.10938,0.32031,0.125,0.32812,0.13281,0.33594,0.14062,0.35156,0.15625,0.35938,0.16406,0.375,0.17969,0.38281,0.19531], + "ImageOrientationPatientDICOM": [0.999294,0.0318495,-0.0199537,-0.0362931,0.955685,-0.292144], + "InPlanePhaseEncodingDirectionDICOM": "COL", + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20210317", + "generatedfrom": "GRE_FIELD_MAPPING_AP_0016" +} diff --git a/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-AP_run-1_magnitude2.nii.gz b/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-AP_run-1_magnitude2.nii.gz new file mode 100644 index 00000000..e69de29b diff --git a/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-AP_run-1_phasediff.json b/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-AP_run-1_phasediff.json new file mode 100644 index 00000000..bc3614fb --- /dev/null +++ b/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-AP_run-1_phasediff.json @@ -0,0 +1,64 @@ +{ + "Modality": "MR", + "MagneticFieldStrength": 3, + "ImagingFrequency": 123.251, + "Manufacturer": "Siemens", + "ManufacturersModelName": "Prisma", + "InstitutionName": "Rigshospitalet", + "InstitutionalDepartmentName": "Department of Neurology", + "InstitutionAddress": "Blegdamsvej 9,Copenhagen,District,DK,2100", + "DeviceSerialNumber": "166141", + "StationName": "RHRTGSIEMR01", + "BodyPartExamined": "BRAIN", + "PatientPosition": "HFS", + "ProcedureStepDescription": "MR projekt - neuro", + "SoftwareVersions": "syngo MR E11", + "MRAcquisitionType": "2D", + "SeriesDescription": "gre_field_mapping_AP", + "ProtocolName": "gre_field_mapping_AP", + "ScanningSequence": "GR", + "SequenceVariant": "SP", + "SequenceName": "*fm2d2r", + "ImageType": [ + "ORIGINAL", + "PRIMARY", + "P", + "ND", + "PHASE" + ], + "SeriesNumber": 17, + "AcquisitionTime": "09:23:39.270000", + "AcquisitionNumber": 1, + "SliceThickness": 3, + "SpacingBetweenSlices": 3.6, + "SAR": 0.08852649999999999, + "EchoNumber": 2, + "EchoTime": 0.00738, + "RepetitionTime": 0.4, + "FlipAngle": 60, + "EchoTime1": 0.00492, + "EchoTime2": 0.00738, + "PartialFourier": 1, + "BaseResolution": 76, + "ShimSetting": [62,-11422,7976,437,-29,369,-92,-16], + "TxRefAmp": 252.164, + "PhaseResolution": 1, + "ReceiveCoilName": "Head_32", + "ReceiveCoilActiveElements": "HEA;HEP", + "PulseSequenceDetails": "%SiemensSeq%\\gre_field_mapping", + "ConsistencyInfo": "N4_VE11E_LATEST_20181129", + "PercentPhaseFOV": 100, + "PercentSampling": 100, + "PhaseEncodingSteps": 76, + "AcquisitionMatrixPE": 76, + "ReconMatrixPE": 76, + "PixelBandwidth": 280, + "DwellTime": 2.34e-05, + "PhaseEncodingDirection": "j-", + "SliceTiming": [0,0.20312,0.00781,0.21094,0.01562,0.22656,0.03125,0.23438,0.03906,0.25,0.05469,0.25781,0.0625,0.27344,0.07811999999999999,0.28125,0.08594,0.29688,0.10156,0.30469,0.10938,0.32031,0.125,0.32812,0.13281,0.33594,0.14062,0.35156,0.15625,0.35938,0.16406,0.375,0.17969,0.38281,0.19531], + "ImageOrientationPatientDICOM": [0.999294,0.0318495,-0.0199537,-0.0362931,0.955685,-0.292144], + "InPlanePhaseEncodingDirectionDICOM": "COL", + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20210317", + "generatedfrom": "GRE_FIELD_MAPPING_AP_0017" +} diff --git a/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-AP_run-1_phasediff.nii.gz b/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-AP_run-1_phasediff.nii.gz new file mode 100644 index 00000000..e69de29b diff --git a/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-LR_run-1_magnitude1.json b/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-LR_run-1_magnitude1.json new file mode 100644 index 00000000..a9b0456e --- /dev/null +++ b/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-LR_run-1_magnitude1.json @@ -0,0 +1,62 @@ +{ + "Modality": "MR", + "MagneticFieldStrength": 3, + "ImagingFrequency": 123.251, + "Manufacturer": "Siemens", + "ManufacturersModelName": "Prisma", + "InstitutionName": "Rigshospitalet", + "InstitutionalDepartmentName": "Department of Neurology", + "InstitutionAddress": "Blegdamsvej 9,Copenhagen,District,DK,2100", + "DeviceSerialNumber": "166141", + "StationName": "RHRTGSIEMR01", + "BodyPartExamined": "BRAIN", + "PatientPosition": "HFS", + "ProcedureStepDescription": "MR projekt - neuro", + "SoftwareVersions": "syngo MR E11", + "MRAcquisitionType": "2D", + "SeriesDescription": "gre_field_mapping", + "ProtocolName": "gre_field_mapping", + "ScanningSequence": "GR", + "SequenceVariant": "SP", + "SequenceName": "*fm2d2r", + "ImageType": [ + "ORIGINAL", + "PRIMARY", + "M", + "ND", + "NORM" + ], + "SeriesNumber": 13, + "AcquisitionTime": "09:22:35.050000", + "AcquisitionNumber": 1, + "SliceThickness": 3, + "SpacingBetweenSlices": 3.6, + "SAR": 0.08852649999999999, + "EchoNumber": 1, + "EchoTime": 0.00492, + "RepetitionTime": 0.4, + "FlipAngle": 60, + "PartialFourier": 1, + "BaseResolution": 76, + "ShimSetting": [62,-11422,7976,437,-29,369,-92,-16], + "TxRefAmp": 252.164, + "PhaseResolution": 1, + "ReceiveCoilName": "Head_32", + "ReceiveCoilActiveElements": "HEA;HEP", + "PulseSequenceDetails": "%SiemensSeq%\\gre_field_mapping", + "ConsistencyInfo": "N4_VE11E_LATEST_20181129", + "PercentPhaseFOV": 100, + "PercentSampling": 100, + "PhaseEncodingSteps": 76, + "AcquisitionMatrixPE": 76, + "ReconMatrixPE": 76, + "PixelBandwidth": 290, + "DwellTime": 2.27e-05, + "PhaseEncodingDirection": "i", + "SliceTiming": [0,0.21094,0.01562,0.21875,0.02344,0.23438,0.03906,0.24219,0.04688,0.25,0.0625,0.26562,0.07031,0.28125,0.08594,0.28906,0.09375,0.30469,0.10938,0.3125,0.11719,0.32031,0.125,0.33594,0.14062,0.34375,0.14844,0.35938,0.16406,0.36719,0.17188,0.38281,0.1875,0.39062,0.19531], + "ImageOrientationPatientDICOM": [0.999294,0.0318495,-0.0199537,-0.0362931,0.955685,-0.292144], + "InPlanePhaseEncodingDirectionDICOM": "ROW", + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20210317", + "generatedfrom": "GRE_FIELD_MAPPING_0013" +} diff --git a/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-LR_run-1_magnitude1.nii.gz b/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-LR_run-1_magnitude1.nii.gz new file mode 100644 index 00000000..e69de29b diff --git a/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-LR_run-1_magnitude2.json b/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-LR_run-1_magnitude2.json new file mode 100644 index 00000000..29ae286a --- /dev/null +++ b/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-LR_run-1_magnitude2.json @@ -0,0 +1,62 @@ +{ + "Modality": "MR", + "MagneticFieldStrength": 3, + "ImagingFrequency": 123.251, + "Manufacturer": "Siemens", + "ManufacturersModelName": "Prisma", + "InstitutionName": "Rigshospitalet", + "InstitutionalDepartmentName": "Department of Neurology", + "InstitutionAddress": "Blegdamsvej 9,Copenhagen,District,DK,2100", + "DeviceSerialNumber": "166141", + "StationName": "RHRTGSIEMR01", + "BodyPartExamined": "BRAIN", + "PatientPosition": "HFS", + "ProcedureStepDescription": "MR projekt - neuro", + "SoftwareVersions": "syngo MR E11", + "MRAcquisitionType": "2D", + "SeriesDescription": "gre_field_mapping", + "ProtocolName": "gre_field_mapping", + "ScanningSequence": "GR", + "SequenceVariant": "SP", + "SequenceName": "*fm2d2r", + "ImageType": [ + "ORIGINAL", + "PRIMARY", + "M", + "ND", + "NORM" + ], + "SeriesNumber": 13, + "AcquisitionTime": "09:22:35.452500", + "AcquisitionNumber": 1, + "SliceThickness": 3, + "SpacingBetweenSlices": 3.6, + "SAR": 0.08852649999999999, + "EchoNumber": 2, + "EchoTime": 0.00738, + "RepetitionTime": 0.4, + "FlipAngle": 60, + "PartialFourier": 1, + "BaseResolution": 76, + "ShimSetting": [62,-11422,7976,437,-29,369,-92,-16], + "TxRefAmp": 252.164, + "PhaseResolution": 1, + "ReceiveCoilName": "Head_32", + "ReceiveCoilActiveElements": "HEA;HEP", + "PulseSequenceDetails": "%SiemensSeq%\\gre_field_mapping", + "ConsistencyInfo": "N4_VE11E_LATEST_20181129", + "PercentPhaseFOV": 100, + "PercentSampling": 100, + "PhaseEncodingSteps": 76, + "AcquisitionMatrixPE": 76, + "ReconMatrixPE": 76, + "PixelBandwidth": 290, + "DwellTime": 2.27e-05, + "PhaseEncodingDirection": "i", + "SliceTiming": [0,0.20312,0.00781,0.21875,0.02344,0.22656,0.03125,0.24219,0.04688,0.25,0.05469,0.26562,0.07031,0.27344,0.07811999999999999,0.28125,0.08594,0.29688,0.10156,0.30469,0.11719,0.32031,0.125,0.32812,0.14062,0.34375,0.14844,0.35156,0.15625,0.36719,0.17188,0.375,0.17969,0.39062,0.19531], + "ImageOrientationPatientDICOM": [0.999294,0.0318495,-0.0199537,-0.0362931,0.955685,-0.292144], + "InPlanePhaseEncodingDirectionDICOM": "ROW", + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20210317", + "generatedfrom": "GRE_FIELD_MAPPING_0013" +} diff --git a/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-LR_run-1_magnitude2.nii.gz b/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-LR_run-1_magnitude2.nii.gz new file mode 100644 index 00000000..e69de29b diff --git a/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-LR_run-1_phasediff.json b/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-LR_run-1_phasediff.json new file mode 100644 index 00000000..36313638 --- /dev/null +++ b/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-LR_run-1_phasediff.json @@ -0,0 +1,64 @@ +{ + "Modality": "MR", + "MagneticFieldStrength": 3, + "ImagingFrequency": 123.251, + "Manufacturer": "Siemens", + "ManufacturersModelName": "Prisma", + "InstitutionName": "Rigshospitalet", + "InstitutionalDepartmentName": "Department of Neurology", + "InstitutionAddress": "Blegdamsvej 9,Copenhagen,District,DK,2100", + "DeviceSerialNumber": "166141", + "StationName": "RHRTGSIEMR01", + "BodyPartExamined": "BRAIN", + "PatientPosition": "HFS", + "ProcedureStepDescription": "MR projekt - neuro", + "SoftwareVersions": "syngo MR E11", + "MRAcquisitionType": "2D", + "SeriesDescription": "gre_field_mapping", + "ProtocolName": "gre_field_mapping", + "ScanningSequence": "GR", + "SequenceVariant": "SP", + "SequenceName": "*fm2d2r", + "ImageType": [ + "ORIGINAL", + "PRIMARY", + "P", + "ND", + "PHASE" + ], + "SeriesNumber": 14, + "AcquisitionTime": "09:22:35.452500", + "AcquisitionNumber": 1, + "SliceThickness": 3, + "SpacingBetweenSlices": 3.6, + "SAR": 0.08852649999999999, + "EchoNumber": 2, + "EchoTime": 0.00738, + "RepetitionTime": 0.4, + "FlipAngle": 60, + "EchoTime1": 0.00492, + "EchoTime2": 0.00738, + "PartialFourier": 1, + "BaseResolution": 76, + "ShimSetting": [62,-11422,7976,437,-29,369,-92,-16], + "TxRefAmp": 252.164, + "PhaseResolution": 1, + "ReceiveCoilName": "Head_32", + "ReceiveCoilActiveElements": "HEA;HEP", + "PulseSequenceDetails": "%SiemensSeq%\\gre_field_mapping", + "ConsistencyInfo": "N4_VE11E_LATEST_20181129", + "PercentPhaseFOV": 100, + "PercentSampling": 100, + "PhaseEncodingSteps": 76, + "AcquisitionMatrixPE": 76, + "ReconMatrixPE": 76, + "PixelBandwidth": 290, + "DwellTime": 2.27e-05, + "PhaseEncodingDirection": "i", + "SliceTiming": [0,0.20312,0.00781,0.21875,0.02344,0.22656,0.03125,0.24219,0.04688,0.25,0.05469,0.26562,0.07031,0.27344,0.07811999999999999,0.28125,0.08594,0.29688,0.10156,0.30469,0.11719,0.32031,0.125,0.32812,0.14062,0.34375,0.14844,0.35156,0.15625,0.36719,0.17188,0.375,0.17969,0.39062,0.19531], + "ImageOrientationPatientDICOM": [0.999294,0.0318495,-0.0199537,-0.0362931,0.955685,-0.292144], + "InPlanePhaseEncodingDirectionDICOM": "ROW", + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20210317", + "generatedfrom": "GRE_FIELD_MAPPING_0014" +} diff --git a/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-LR_run-1_phasediff.nii.gz b/demos/fmap_failexemple/sub-57507/ses-Baseline/fmap/sub-57507_ses-Baseline_acq-gre_dir-LR_run-1_phasediff.nii.gz new file mode 100644 index 00000000..e69de29b diff --git a/demos/notebooks/.gitignore b/demos/notebooks/.gitignore new file mode 100644 index 00000000..972a7fbe --- /dev/null +++ b/demos/notebooks/.gitignore @@ -0,0 +1,3 @@ + +dummy_ds +output diff --git a/demos/notebooks/BIDS-Matlab_01_basics.ipynb b/demos/notebooks/BIDS-Matlab_01_basics.ipynb new file mode 100644 index 00000000..3aa0c3aa --- /dev/null +++ b/demos/notebooks/BIDS-Matlab_01_basics.ipynb @@ -0,0 +1,427 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "# BIDS-Matlab: basics\n", + "\n", + "(C) Copyright 2021 BIDS-MATLAB developers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "add_bids_matlab_to_path();" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will work with the \"empty\" dataset\n", + "from the bids-examples repository :\n", + "https://github.com/bids-standard/bids-examples" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We use a bit of command line magic to view the top (`head`) directories (`-d`) at a certain level depth (`-L 2`)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's work on the `ds101` dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "!tree bids-examples/ds101 -d -L 2 | head" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Indexing a dataset\n", + "\n", + "This is done with the `bids.layout` function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "help bids.layout" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "BIDS = bids.layout(fullfile(pwd,'bids-examples','ds101'));" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Querying a dataset\n", + "\n", + "Make general queries about the dataset are with `bids.query` made on the layout returned by `bids.layout`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "help bids.query" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "entities = bids.query(BIDS, 'entities');\n", + "disp(entities);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "subjects = bids.query(BIDS, 'subjects');\n", + "disp(subjects);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sessions = bids.query(BIDS,'sessions');\n", + "disp(sessions);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "runs = bids.query(BIDS, 'runs');\n", + "disp(runs);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tasks = bids.query(BIDS, 'tasks');\n", + "disp(tasks);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "suffixes = bids.query(BIDS, 'suffixes');\n", + "disp(suffixes);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "modalities = bids.query(BIDS, 'modalities');\n", + "disp(modalities);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "% Make more specific queries\n", + "runs = bids.query(BIDS, 'runs', 'suffix', 'T1w');\n", + "disp(runs);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "runs = bids.query(BIDS, 'runs', 'suffix', 'bold');\n", + "disp(runs);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Get filenames" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Get the NIfTI file for subject `'05'`, run `'02'` and task `'Simontask'`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "data = bids.query(BIDS, 'data', 'sub', '05', 'run', '02', 'task', 'Simontask', 'suffix', 'bold');\n", + "disp(data);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " Note that for the entities listed below can be queried using integers:\n", + " - `'run'`\n", + " - `'flip'`\n", + " - `'inv'`\n", + " - `'split'`\n", + " - `'echo'`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This can be also done by creating a structure that can be used as a library." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "filter = struct(...\n", + " 'sub','05', ...\n", + " 'run', 1:3, ...\n", + " 'task','Simontask', ...\n", + " 'suffix','bold');" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = bids.query(BIDS, 'data', filter);\n", + "disp(data);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also query data from several labels or indices" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "filter.sub = {'01', '03'}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = bids.query(BIDS, 'data', filter);\n", + "disp(data);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Get metadata" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also get the metadata of that file including TR:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "metadata = bids.query(BIDS, 'metadata', filter);\n", + "disp(metadata);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Get the T1-weighted images from all subjects:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = bids.query(BIDS, 'data', 'suffix', 'T1w');\n", + "disp(data);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Get \"dependencies\" of a given file" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This can be useful to find the files that are associated with the file you just queried.\n", + "\n", + "In this case the events file for a BOLD run." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "filter = struct('sub','05', ...\n", + " 'run','02', ...\n", + " 'task','Simontask', ...\n", + " 'suffix','bold');\n", + " \n", + "dependencies = bids.query(BIDS, 'dependencies', filter);\n", + "disp(dependencies);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Using regular expressions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When using `bids.query` it is possible to use regular expressions. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "filter = struct('sub','0[1-5]', ...\n", + " 'run','02', ...\n", + " 'task','Simon*', ...\n", + " 'suffix','bold');\n", + " \n", + "filter = struct('sub','0[1-3]', ...\n", + " 'run','02', ...\n", + " 'task','Sim.*', ...\n", + " 'suffix','bold'); \n", + " \n", + "data = bids.query(BIDS, 'data', filter);\n", + "disp(data); " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Octave", + "language": "octave", + "name": "octave" + }, + "language_info": { + "file_extension": ".m", + "help_links": [ + { + "text": "GNU Octave", + "url": "https://www.gnu.org/software/octave/support.html" + }, + { + "text": "Octave Kernel", + "url": "https://github.com/Calysto/octave_kernel" + }, + { + "text": "MetaKernel Magics", + "url": "https://metakernel.readthedocs.io/en/latest/source/README.html" + } + ], + "mimetype": "text/x-octave", + "name": "octave", + "version": "6.4.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/demos/notebooks/BIDS-Matlab_02_derivatives.ipynb b/demos/notebooks/BIDS-Matlab_02_derivatives.ipynb new file mode 100644 index 00000000..70ebf6a2 --- /dev/null +++ b/demos/notebooks/BIDS-Matlab_02_derivatives.ipynb @@ -0,0 +1,281 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "# BIDS-Matlab: derivatives\n", + "\n", + "(C) Copyright 2021 BIDS-MATLAB developers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "add_bids_matlab_to_path();" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Indexing derivatives\n", + "\n", + "Let's work on an `fmriprep` dataset.\n", + "\n", + "To work with derivatives data, we must ignore the BIDS schema for indexing." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "use_schema = false();\n", + "\n", + "BIDS = bids.layout(fullfile(pwd, 'bids-examples', 'ds000001-fmriprep'), ...\n", + " 'use_schema', false);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "modalities = bids.query(BIDS, 'modalities');\n", + "disp(modalities);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The dataset description `DatasetType` confirms we are working with a derivative dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "disp(BIDS.description);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can access any preprocessed data by querying\n", + "for data described (`desc` entity) as preprocessed (`preproc`)\n", + "and maybe also in which `space` we want to work in." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "data = bids.query(BIDS, 'data', 'modality', 'anat', 'desc', 'preproc', 'space', 'MNI152NLin2009cAsym');\n", + "disp(data);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "But we can also get the surface data from Freesurfer.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "data = bids.query(BIDS, 'data', 'sub', '10', 'modality', 'func', 'space', 'fsaverage5');\n", + "disp(data);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "data = bids.query(BIDS, 'data', 'sub', '10', 'desc', 'confounds');\n", + "disp(data);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also directly look up json files when we don't use the BIDS schema." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "extensions = bids.query(BIDS, 'extensions');\n", + "disp(extensions);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "filter.sub = '10';\n", + "data = bids.query(BIDS, 'data', filter);\n", + "disp(data);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "filter.space = 'MNI152NLin2009cAsym';\n", + "filter.desc = 'preproc';\n", + "filter.run = '3';\n", + "metadata = bids.query(BIDS, 'metadata', filter);\n", + "disp(metadata);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Indexing nested derivatives" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "BIDS = bids.layout(fullfile(pwd,'bids-examples', 'ds000117'), ...\n", + " 'use_schema', false, ...\n", + " 'index_derivatives', true);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bids.query(BIDS.derivatives.meg_derivatives, 'subjects');" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "## Copying a raw dataset to start a new analysis\n", + "\n", + "Let's work on an `fmriprep` dataset.\n", + "\n", + "To work with derivatives data, we must ignore the BIDS schema for indexing." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dataset = fullfile(pwd, 'bids-examples', 'qmri_vfa');\n", + "\n", + "output_path = fullfile(pwd, 'output')\n", + "\n", + "filter = struct('modality', 'anat');\n", + "\n", + "pipeline_name = 'SPM12';\n", + "\n", + "bids.copy_to_derivative(dataset, ...\n", + " 'pipeline_name', pipeline_name, ...\n", + " 'out_path', output_path, ...\n", + " 'filter', filter, ...\n", + " 'force', true, ...\n", + " 'unzip', false, ...\n", + " 'verbose', true);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "BIDS = bids.layout(fullfile(output_path, 'SPM12'));\n", + "BIDS.description.GeneratedBy;" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Octave", + "language": "octave", + "name": "octave" + }, + "language_info": { + "file_extension": ".m", + "help_links": [ + { + "text": "GNU Octave", + "url": "https://www.gnu.org/software/octave/support.html" + }, + { + "text": "Octave Kernel", + "url": "https://github.com/Calysto/octave_kernel" + }, + { + "text": "MetaKernel Magics", + "url": "https://metakernel.readthedocs.io/en/latest/source/README.html" + } + ], + "mimetype": "text/x-octave", + "name": "octave", + "version": "4.2.2" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/demos/notebooks/BIDS-Matlab_03_filenames_and_metadata.ipynb b/demos/notebooks/BIDS-Matlab_03_filenames_and_metadata.ipynb new file mode 100644 index 00000000..a00c5142 --- /dev/null +++ b/demos/notebooks/BIDS-Matlab_03_filenames_and_metadata.ipynb @@ -0,0 +1,292 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Create filenames, filepaths, and JSON\n", + "\n", + "(C) Copyright 2021 BIDS-MATLAB developers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "add_bids_matlab_to_path();" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Generating filenames\n", + "\n", + "The vast majority of BIDS filenames have the following pattern:\n", + "\n", + "- a series of `entity-label` pairs separated by `_`\n", + "- a final `_suffix`\n", + "- a file `.extension`\n", + "- pseudo \"regular expression\" : `entity-label(_entity-label)+_suffix.extension`\n", + "\n", + "`entity`, `label`, `suffix`, `extension` are alphanumeric only (no special character): `([a-zA-Z0-9])+`\n", + "\n", + " - For example, suffixes can be `T1w` or `bold` but not `T1w_skullstripped` (no underscore allowed).\n", + "\n", + "Entity and label are separated by a dash: `entity-label --> ([a-zA-Z0-9])+-([a-zA-Z0-9])+`\n", + " \n", + " - For example, you can have: `sub-01` but not `sub-01-blind`\n", + "\n", + "Entity-label pairs are separated by an underscore:\n", + "\n", + " `entity-label(_entity-label)+ --> ([a-zA-Z0-9])+-([a-zA-Z0-9])+(_([a-zA-Z0-9])+-([a-zA-Z0-9])+)+`\n", + "\n", + "**Prefixes are not a thing in official BIDS names**\n", + "\n", + "\n", + "BIDS has a number of \n", + "[officially recognised entities](https://bids-specification.readthedocs.io/en/stable/99-appendices/04-entity-table.html) \n", + "(`sub`, `ses`, `task`...) that must come in a specific order for each suffix.\n", + "\n", + "BIDS derivatives adds a few more entities (`desc`, `space`, `res`...) \n", + "and suffixes (`pseg`, `dseg`, `mask`...) \n", + "that can be used to name and describe preprocessed data." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `bids.File` class can help generate BIDS valid file names." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "input = struct('ext', '.nii');\n", + "input.suffix = 'bold';\n", + "input.entities = struct('sub', '01', ...\n", + " 'task', 'faceRecognition', ...\n", + " 'run', '02', ...\n", + " 'ses', 'test');" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "file = bids.File(input);\n", + "\n", + "file.filename;" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can rely on the BIDS schema to know in which order the entities must go for a certain `suffix` type. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "file = bids.File(input, 'use_schema', true);\n", + "\n", + "file.filename;" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This can also tell you if you are missing a required entity if you set `tolerant` to `false`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "input = struct('ext', '.nii');\n", + "input.suffix = 'bold';\n", + "input.entities = struct('sub', '01', ...\n", + " 'ses', 'test', ...\n", + " 'run', '02');\n", + "\n", + "% uncomment the line below to see the error\n", + "% file = bids.File(input, 'use_schema', true, 'tolerant', false);\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Or you can specify the order of the entities manually." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "input = struct('ext', '.nii');\n", + "input.suffix = 'bold';\n", + "input.entities = struct('sub', '01', ...\n", + " 'task', 'face recognition', ...\n", + " 'run', '02', ...\n", + " 'ses', 'test');\n", + "file = bids.File(input);\n", + "\n", + "entity_order = {'run', 'sub', 'ses'};\n", + "\n", + "file = file.reorder_entities(entity_order);\n", + "file.filename;\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Modifying filenames" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This can be used:\n", + "- to add, remove, modify any of the entities\n", + "- change the suffix\n", + "- change the extensions\n", + "- add or remove any prefix\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "input = 'sub-01_ses-mri_T1w.nii';\n", + "file = bids.File(input, 'use_schema', false);\n", + "\n", + "file.suffix = 'mask';\n", + "file.entities.ses = '';\n", + "file.entities.desc = 'brain';\n", + "\n", + "file.filename;" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Generating file names for derivatives" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This can also be useful to remove the prefix of some files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "input = 'wuasub-01_ses-test_task-faceRecognition_run-02_bold.nii';\n", + "\n", + "file = bids.File(input, 'use_schema', false);\n", + "file.prefix = '';\n", + "file.entities.space = 'IXI549Space';\n", + "file.entities.desc = 'preproc';\n", + "\n", + "file.filename;" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This can prove useful to get a dummy json that should accompany any derivatives files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "json = bids.derivatives_json(file.filename);\n", + "\n", + "json.filename;\n", + "json.content;" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The content of the JSON should adapt depending on the entities or suffix present in the output filename." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "json = bids.derivatives_json('sub-01_ses-test_task-faceRecognition_res-r2pt0_space-IXI549Space_desc-brain_mask.nii')\n", + "json.filename;\n", + "json.content;\n", + "json.content.Resolution{1}{1};" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Octave", + "language": "octave", + "name": "octave" + }, + "language_info": { + "file_extension": ".m", + "help_links": [ + { + "text": "GNU Octave", + "url": "https://www.gnu.org/software/octave/support.html" + }, + { + "text": "Octave Kernel", + "url": "https://github.com/Calysto/octave_kernel" + }, + { + "text": "MetaKernel Magics", + "url": "https://metakernel.readthedocs.io/en/latest/source/README.html" + } + ], + "mimetype": "text/x-octave", + "name": "octave", + "version": "4.2.2" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/demos/notebooks/BIDS-Matlab_04_new_datasets.ipynb b/demos/notebooks/BIDS-Matlab_04_new_datasets.ipynb new file mode 100644 index 00000000..e639e0a7 --- /dev/null +++ b/demos/notebooks/BIDS-Matlab_04_new_datasets.ipynb @@ -0,0 +1,142 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Create filenames, filepaths, and JSON\n", + "\n", + "(C) Copyright 2021 BIDS-MATLAB developers\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "add_bids_matlab_to_path();" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initialising a new BIDS dataset" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This can be useful when you are going to output your analysis or your data acquisition into a new dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "help bids.init" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Derivatives datasets have some extra info in their `dataset_description.json`.\n", + "\n", + "If you are going to curate the dataset with \n", + "[Datalad](http://handbook.datalad.org/en/latest/), \n", + "you can also mention it and this will modify the README \n", + "to add extra info about this (taken from the datalad handbook)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pth = fullfile(pwd, 'dummy_ds');\n", + "\n", + "folders.subjects = {'01', '02'};\n", + "folders.sessions = {'pre', 'post'};\n", + "folders.modalities = {'anat', 'eeg'};" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bids.init(pth, 'folders', folders, 'is_derivative', true, 'is_datalad_ds', true);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!tree dummy_ds" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Template README was generated." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!cat dummy_ds/README" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!cat dummy_ds/dataset_description.json" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Octave", + "language": "octave", + "name": "octave" + }, + "language_info": { + "file_extension": ".m", + "help_links": [ + { + "text": "GNU Octave", + "url": "https://www.gnu.org/software/octave/support.html" + }, + { + "text": "Octave Kernel", + "url": "https://github.com/Calysto/octave_kernel" + }, + { + "text": "MetaKernel Magics", + "url": "https://metakernel.readthedocs.io/en/latest/source/README.html" + } + ], + "mimetype": "text/x-octave", + "name": "octave", + "version": "4.2.2" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/demos/notebooks/BIDS-Matlab_05_report.ipynb b/demos/notebooks/BIDS-Matlab_05_report.ipynb new file mode 100644 index 00000000..8a68a9aa --- /dev/null +++ b/demos/notebooks/BIDS-Matlab_05_report.ipynb @@ -0,0 +1,76 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# BIDS-matlab: reports\n", + "\n", + "(C) Copyright 2021 BIDS-MATLAB developers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "add_bids_matlab_to_path();" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "BIDS = bids.layout(fullfile(pwd, 'bids-examples', 'ds101'));" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "read_nifti = false;\n", + "output_path = fullfile(pwd, 'output');\n", + "verbose = true;\n", + "\n", + "bids.report(BIDS, ...\n", + " 'output_path', output_path, ...\n", + " 'read_nifti', read_nifti, ...\n", + " 'verbose', verbose);" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Octave", + "language": "octave", + "name": "octave" + }, + "language_info": { + "file_extension": ".m", + "help_links": [ + { + "text": "GNU Octave", + "url": "https://www.gnu.org/software/octave/support.html" + }, + { + "text": "Octave Kernel", + "url": "https://github.com/Calysto/octave_kernel" + }, + { + "text": "MetaKernel Magics", + "url": "https://metakernel.readthedocs.io/en/latest/source/README.html" + } + ], + "mimetype": "text/x-octave", + "name": "octave", + "version": "4.2.2" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/demos/notebooks/BIDS-Matlab_06_tsv_json.ipynb b/demos/notebooks/BIDS-Matlab_06_tsv_json.ipynb new file mode 100644 index 00000000..a55e98ea --- /dev/null +++ b/demos/notebooks/BIDS-Matlab_06_tsv_json.ipynb @@ -0,0 +1,183 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6d502f01", + "metadata": {}, + "source": [ + "# BIDS-Matlab: TSV and JSON files\n", + "\n", + "(C) Copyright 2021 BIDS-MATLAB developers\n", + "\n", + "## Read from TSV files\n", + "\n", + "This can be done with the `bids.util.tsvread` function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "92b7a41a", + "metadata": {}, + "outputs": [], + "source": [ + "add_bids_matlab_to_path();" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ec10866", + "metadata": {}, + "outputs": [], + "source": [ + "BIDS = bids.layout(fullfile(pwd,'bids-examples','ieeg_visual'));" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59ed8d9a", + "metadata": {}, + "outputs": [], + "source": [ + "bids.query(BIDS, 'subjects');\n", + "bids.query(BIDS, 'tasks');\n", + "events_file = bids.query(BIDS, 'data', 'sub', '01', 'task', 'visual', 'suffix', 'events');" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d605782", + "metadata": {}, + "outputs": [], + "source": [ + "bids.util.tsvread(events_file{1});" + ] + }, + { + "cell_type": "markdown", + "id": "ebe684a3", + "metadata": {}, + "source": [ + "## Write to TSV files" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e36c8029", + "metadata": {}, + "outputs": [], + "source": [ + "tsv_file = fullfile(pwd, 'output', 'sub-01_task-STRUCTURE_events.tsv');\n", + "\n", + "logFile.onset = [2; NaN];\n", + "logFile.trial_type = {'motion_up'; 'static'};\n", + "logFile.duration = [1; 4];\n", + "logFile.speed = [NaN; 4];\n", + "logFile.is_fixation = {'true'; '3'};\n", + "\n", + "bids.util.tsvwrite(tsv_file, logFile);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5214322", + "metadata": {}, + "outputs": [], + "source": [ + "!cat output/sub-01_task-STRUCTURE_events.tsv" + ] + }, + { + "cell_type": "markdown", + "id": "792e2f13", + "metadata": {}, + "source": [ + "## Write to JSON files" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "abc73827", + "metadata": {}, + "outputs": [], + "source": [ + "content = struct( 'Name', 'test', ...\n", + " 'BIDSVersion', '1.6', ...\n", + " 'DatasetType', 'raw', ...\n", + " 'License', '', ...\n", + " 'Acknowledgements', '', ...\n", + " 'HowToAcknowledge', '', ...\n", + " 'DatasetDOI', '', ...\n", + " 'HEDVersion', '', ...\n", + " 'Funding', {{}}, ...\n", + " 'Authors', {{}}, ...\n", + " 'ReferencesAndLinks', {{}});\n", + "\n", + "bids.util.jsonencode(fullfile(pwd, 'output', 'dataset_description.json'), content);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "daae43aa", + "metadata": {}, + "outputs": [], + "source": [ + "!cat output/dataset_description.json" + ] + }, + { + "cell_type": "markdown", + "id": "ea84dfb0", + "metadata": {}, + "source": [ + "## Read from JSON files" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ae689e2a", + "metadata": {}, + "outputs": [], + "source": [ + "bids.util.jsondecode(fullfile(pwd, 'output', 'dataset_description.json'));" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Octave", + "language": "octave", + "name": "octave" + }, + "language_info": { + "file_extension": ".m", + "help_links": [ + { + "text": "GNU Octave", + "url": "https://www.gnu.org/software/octave/support.html" + }, + { + "text": "Octave Kernel", + "url": "https://github.com/Calysto/octave_kernel" + }, + { + "text": "MetaKernel Magics", + "url": "https://metakernel.readthedocs.io/en/latest/source/README.html" + } + ], + "mimetype": "text/x-octave", + "name": "octave", + "version": "4.2.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/demos/notebooks/BIDS-Matlab_07_diagnostic.ipynb b/demos/notebooks/BIDS-Matlab_07_diagnostic.ipynb new file mode 100644 index 00000000..d4fb6f5b --- /dev/null +++ b/demos/notebooks/BIDS-Matlab_07_diagnostic.ipynb @@ -0,0 +1,80 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# BIDS-matlab: diagnostic\n", + "\n", + "(C) Copyright 2021 BIDS-MATLAB developers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "add_bids_matlab_to_path();" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "BIDS = bids.layout(fullfile(pwd, 'bids-examples', 'ds000247'));" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "diagnostic_table = bids.diagnostic(BIDS);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "diagnostic_table = bids.diagnostic(BIDS, 'split_by', {'task'});" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Octave", + "language": "octave", + "name": "octave" + }, + "language_info": { + "file_extension": ".m", + "help_links": [ + { + "text": "GNU Octave", + "url": "https://www.gnu.org/software/octave/support.html" + }, + { + "text": "Octave Kernel", + "url": "https://github.com/Calysto/octave_kernel" + }, + { + "text": "MetaKernel Magics", + "url": "https://metakernel.readthedocs.io/en/latest/source/README.html" + } + ], + "mimetype": "text/x-octave", + "name": "octave", + "version": "4.2.2" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/demos/notebooks/BIDS-Matlab_08_stats_model.ipynb b/demos/notebooks/BIDS-Matlab_08_stats_model.ipynb new file mode 100644 index 00000000..14a8ab74 --- /dev/null +++ b/demos/notebooks/BIDS-Matlab_08_stats_model.ipynb @@ -0,0 +1 @@ +{"cells":[{"cell_type":"markdown","id":"0df7bd02","metadata":{},"source":["(C) Copyright 2021 BIDS-MATLAB developers"]},{"cell_type":"code","execution_count":null,"id":"357804c7-6a9c-48e0-8323-0f3cb1c7fb2d","metadata":{"trusted":true},"outputs":[],"source":["add_bids_matlab_to_path();"]},{"cell_type":"code","execution_count":null,"id":"d1f587ac-9c43-418e-952b-39d6c69e4b5f","metadata":{"trusted":true},"outputs":[],"source":["% Create an empty model\n","bm = bids.Model('init', true);\n","filename = fullfile(pwd, 'model-empty_smdl.json');\n","bm.write(filename);"]},{"cell_type":"code","execution_count":null,"id":"7757a4b2-5262-4202-91ed-261db0406b8a","metadata":{"trusted":true},"outputs":[],"source":["!cat model-empty_smdl.json"]},{"cell_type":"code","execution_count":null,"id":"5513ef68-a25d-4f2e-a08a-259911aaa481","metadata":{"trusted":true},"outputs":[],"source":["% create a default bids model for a dataset\n","ds = fullfile(pwd, 'bids-examples', 'ds003');\n","BIDS = bids.layout(ds);\n","\n","bm = bids.Model();\n","bm = bm.default(BIDS);\n","\n","filename = fullfile(pwd, 'model-rhymejudgement_smdl.json');\n","bm.write(filename);"]},{"cell_type":"code","execution_count":null,"id":"21e76a7f-369f-4d80-a101-d0623c359371","metadata":{"trusted":true},"outputs":[],"source":["!cat model-rhymejudgement_smdl.json"]},{"cell_type":"code","execution_count":null,"id":"865160ad-3d83-4149-a575-0aace7a123b9","metadata":{"trusted":true},"outputs":[],"source":["% load and query a specific model file\n","model_file = fullfile('..', '..', 'tests', 'data', 'model', 'model-narps_smdl.json')\n","bm = bids.Model('file', model_file);"]},{"cell_type":"code","execution_count":null,"id":"2d7aa487-7c9c-4613-909d-b5ba675d5370","metadata":{"trusted":true},"outputs":[],"source":["bm.get_nodes('Level', 'Run')"]},{"cell_type":"code","execution_count":null,"id":"100a006c-ec0c-42fd-978e-1b4470e58134","metadata":{"trusted":true},"outputs":[],"source":["bm.get_transformations('Level', 'Run')\n","bm.get_dummy_contrasts('Level', 'Run')\n","bm.get_contrasts('Level', 'Run')\n","bm.get_model('Level', 'Run')\n","bm.get_design_matrix('Level', 'Run')"]}],"metadata":{"kernelspec":{"display_name":"Octave","language":"octave","name":"octave"},"language_info":{"file_extension":".m","help_links":[{"text":"GNU Octave","url":"https://www.gnu.org/software/octave/support.html"},{"text":"Octave Kernel","url":"https://github.com/Calysto/octave_kernel"},{"text":"MetaKernel Magics","url":"https://metakernel.readthedocs.io/en/latest/source/README.html"}],"mimetype":"text/x-octave","name":"octave","version":"4.2.2"}},"nbformat":4,"nbformat_minor":5} diff --git a/demos/notebooks/BIDS_Matlab_01_basics.m b/demos/notebooks/BIDS_Matlab_01_basics.m new file mode 100644 index 00000000..b86ed14c --- /dev/null +++ b/demos/notebooks/BIDS_Matlab_01_basics.m @@ -0,0 +1,178 @@ +% # BIDS-Matlab: basics +% +% (C) Copyright 2021 BIDS-MATLAB developers + +%% + +add_bids_matlab_to_path(); + +% We will work with the "empty" dataset +% from the bids-examples repository : +% https://github.com/bids-standard/bids-examples + +% We use a bit of command line magic to view the top (`head`) directories (`-d`) at a certain level depth (`-L 2`). + +% Let's work on the `ds101` dataset. + +%% + +!tree bids-examples/ds101 -d -L 2 | head + +% ## Indexing a dataset +% +% This is done with the `bids.layout` function. + +%% + +help bids.layout; + +%% + +BIDS = bids.layout(fullfile(pwd, 'bids-examples', 'ds101')); + +% ## Querying a dataset +% +% Make general queries about the dataset are with `bids.query` made on the layout returned by `bids.layout`. + +%% + +help bids.query; + +%% + +entities = bids.query(BIDS, 'entities'); +disp(entities); + +%% + +subjects = bids.query(BIDS, 'subjects'); +disp(subjects); + +%% + +sessions = bids.query(BIDS, 'sessions'); +disp(sessions); + +%% + +runs = bids.query(BIDS, 'runs'); +disp(runs); + +%% + +tasks = bids.query(BIDS, 'tasks'); +disp(tasks); + +%% + +suffixes = bids.query(BIDS, 'suffixes'); +disp(suffixes); + +%% + +modalities = bids.query(BIDS, 'modalities'); +disp(modalities); + +%% + +% Make more specific queries +runs = bids.query(BIDS, 'runs', 'suffix', 'T1w'); +disp(runs); + +%% + +runs = bids.query(BIDS, 'runs', 'suffix', 'bold'); +disp(runs); + +% ### Get filenames + +% Get the NIfTI file for subject `'05'`, run `'02'` and task `'Simontask'`: + +%% + +data = bids.query(BIDS, 'data', 'sub', '05', 'run', '02', 'task', 'Simontask', 'suffix', 'bold'); +disp(data); + +% Note that for the entities listed below can be queried using integers: +% - `'run'` +% - `'flip'` +% - `'inv'` +% - `'split'` +% - `'echo'` + +% This can be also done by creating a structure that can be used as a library. + +%% + +filter = struct( ... + 'sub', '05', ... + 'run', 1:3, ... + 'task', 'Simontask', ... + 'suffix', 'bold'); + +%% + +data = bids.query(BIDS, 'data', filter); +disp(data); + +% You can also query data from several labels or indices + +%% + +filter.sub = {'01', '03'}; + +%% + +data = bids.query(BIDS, 'data', filter); +disp(data); + +% ### Get metadata + +% We can also get the metadata of that file including TR: + +%% + +metadata = bids.query(BIDS, 'metadata', filter); +disp(metadata); + +% Get the T1-weighted images from all subjects: + +%% + +data = bids.query(BIDS, 'data', 'suffix', 'T1w'); +disp(data); + +% ### Get "dependencies" of a given file + +% This can be useful to find the files that are associated with the file you just queried. +% +% In this case the events file for a BOLD run. + +%% + +filter = struct('sub', '05', ... + 'run', '02', ... + 'task', 'Simontask', ... + 'suffix', 'bold'); + +dependencies = bids.query(BIDS, 'dependencies', filter); +disp(dependencies); + +% ### Using regular expressions + +% When using `bids.query` it is possible to use regular expressions. + +%% + +filter = struct('sub', '0[1-5]', ... + 'run', '02', ... + 'task', 'Simon*', ... + 'suffix', 'bold'); + +filter = struct('sub', '0[1-3]', ... + 'run', '02', ... + 'task', 'Sim.*', ... + 'suffix', 'bold'); + +data = bids.query(BIDS, 'data', filter); +disp(data); diff --git a/demos/notebooks/BIDS_Matlab_02_derivatives.m b/demos/notebooks/BIDS_Matlab_02_derivatives.m new file mode 100644 index 00000000..8f883eec --- /dev/null +++ b/demos/notebooks/BIDS_Matlab_02_derivatives.m @@ -0,0 +1,115 @@ +% # BIDS-Matlab: derivatives +% +% (C) Copyright 2021 BIDS-MATLAB developers + +%% + +add_bids_matlab_to_path(); + +% ## Indexing derivatives +% +% Let's work on an `fmriprep` dataset. +% +% To work with derivatives data, we must ignore the BIDS schema for indexing. + +%% + +use_schema = false(); + +BIDS = bids.layout(fullfile(pwd, 'bids-examples', 'ds000001-fmriprep'), ... + 'use_schema', false); + +%% + +modalities = bids.query(BIDS, 'modalities'); +disp(modalities); + +% The dataset description `DatasetType` confirms we are working with a derivative dataset. + +%% + +disp(BIDS.description); + +% We can access any preprocessed data by querying +% for data described (`desc` entity) as preprocessed (`preproc`) +% and maybe also in which `space` we want to work in. + +%% + +data = bids.query(BIDS, 'data', 'modality', 'anat', 'desc', 'preproc', 'space', 'MNI152NLin2009cAsym'); +disp(data); + +% +% But we can also get the surface data from Freesurfer. + +%% + +data = bids.query(BIDS, 'data', 'sub', '10', 'modality', 'func', 'space', 'fsaverage5'); +disp(data); + +%% + +data = bids.query(BIDS, 'data', 'sub', '10', 'desc', 'confounds'); +disp(data); + +% We can also directly look up json files when we don't use the BIDS schema. + +%% + +extensions = bids.query(BIDS, 'extensions'); +disp(extensions); + +%% + +filter.sub = '10'; +data = bids.query(BIDS, 'data', filter); +disp(data); + +%% + +filter.space = 'MNI152NLin2009cAsym'; +filter.desc = 'preproc'; +filter.run = '3'; +metadata = bids.query(BIDS, 'metadata', filter); +disp(metadata); + +% ## Indexing nested derivatives + +%% + +BIDS = bids.layout(fullfile(pwd, 'bids-examples', 'ds000117'), ... + 'use_schema', false, ... + 'index_derivatives', true); + +%% + +bids.query(BIDS.derivatives.meg_derivatives, 'subjects'); + +% ## Copying a raw dataset to start a new analysis +% +% Let's work on an `fmriprep` dataset. +% +% To work with derivatives data, we must ignore the BIDS schema for indexing. + +%% + +dataset = fullfile(pwd, 'bids-examples', 'qmri_vfa'); + +output_path = fullfile(pwd, 'output'); + +filter = struct('modality', 'anat'); + +pipeline_name = 'SPM12'; + +bids.copy_to_derivative(dataset, ... + 'pipeline_name', pipeline_name, ... + 'out_path', output_path, ... + 'filter', filter, ... + 'force', true, ... + 'unzip', false, ... + 'verbose', true); + +%% + +BIDS = bids.layout(fullfile(output_path, 'SPM12')); +BIDS.description.GeneratedBy; diff --git a/demos/notebooks/BIDS_Matlab_03_filenames_and_metadata.m b/demos/notebooks/BIDS_Matlab_03_filenames_and_metadata.m new file mode 100644 index 00000000..04ebdbba --- /dev/null +++ b/demos/notebooks/BIDS_Matlab_03_filenames_and_metadata.m @@ -0,0 +1,146 @@ +% # Create filenames, filepaths, and JSON +% +% (C) Copyright 2021 BIDS-MATLAB developers + +%% + +add_bids_matlab_to_path(); + +% ## Generating filenames +% +% The vast majority of BIDS filenames have the following pattern: +% +% - a series of `entity-label` pairs separated by `_` +% - a final `_suffix` +% - a file `.extension` +% - pseudo "regular expression" : `entity-label(_entity-label)+_suffix.extension` +% +% `entity`, `label`, `suffix`, `extension` are alphanumeric only (no special character): `()+` +% +% - For example, suffixes can be `T1w` or `bold` but not `T1w_skullstripped` (no underscore allowed). +% +% Entity and label are separated by a dash: `entity-label --> ()+-()+` +% +% - For example, you can have: `sub-01` but not `sub-01-blind` +% +% Entity-label pairs are separated by an underscore: +% +% `entity-label(_entity-label)+ --> ()+-()+(_()+-()+)+` +% +% **Prefixes are not a thing in official BIDS names** +% +% +% BIDS has a number of +% (https://bids-specification.readthedocs.io/en/stable/99-appendices/04-entity-table.html) +% (`sub`, `ses`, `task`...) that must come in a specific order for each suffix. +% +% BIDS derivatives adds a few more entities (`desc`, `space`, `res`...) +% and suffixes (`pseg`, `dseg`, `mask`...) +% that can be used to name and describe preprocessed data. + +% The `bids.File` class can help generate BIDS valid file names. + +%% + +input = struct('ext', '.nii'); +input.suffix = 'bold'; +input.entities = struct('sub', '01', ... + 'task', 'faceRecognition', ... + 'run', '02', ... + 'ses', 'test'); + +%% + +file = bids.File(input); + +file.filename; + +% You can rely on the BIDS schema to know in which order the entities must go for a certain `suffix` type. + +%% + +file = bids.File(input, 'use_schema', true); + +file.filename; + +% This can also tell you if you are missing a required entity if you set `tolerant` to `false`. + +%% + +input = struct('ext', '.nii'); +input.suffix = 'bold'; +input.entities = struct('sub', '01', ... + 'ses', 'test', ... + 'run', '02'); + +% uncomment the line below to see the error +% file = bids.File(input, 'use_schema', true, 'tolerant', false); + +% Or you can specify the order of the entities manually. + +%% + +input = struct('ext', '.nii'); +input.suffix = 'bold'; +input.entities = struct('sub', '01', ... + 'task', 'face recognition', ... + 'run', '02', ... + 'ses', 'test'); +file = bids.File(input); + +entity_order = {'run', 'sub', 'ses'}; + +file = file.reorder_entities(entity_order); +file.filename; + +% ## Modifying filenames + +% This can be used: +% - to add, remove, modify any of the entities +% - change the suffix +% - change the extensions +% - add or remove any prefix + +%% + +input = 'sub-01_ses-mri_T1w.nii'; +file = bids.File(input, 'use_schema', false); + +file.suffix = 'mask'; +file.entities.ses = ''; +file.entities.desc = 'brain'; + +file.filename; + +% ## Generating file names for derivatives + +% This can also be useful to remove the prefix of some files. + +%% + +input = 'wuasub-01_ses-test_task-faceRecognition_run-02_bold.nii'; + +file = bids.File(input, 'use_schema', false); +file.prefix = ''; +file.entities.space = 'IXI549Space'; +file.entities.desc = 'preproc'; + +file.filename; + +% This can prove useful to get a dummy json that should accompany any derivatives files. + +%% + +json = bids.derivatives_json(file.filename); + +json.filename; +json.content; + +% The content of the JSON should adapt depending on the entities or suffix present in the output filename. + +%% + +json = bids.derivatives_json('sub-01_ses-test_task-faceRecognition_res-r2pt0_space-IXI549Space_desc-brain_mask.nii'); +json.filename; +json.content; +json.content.Resolution{1}{1}; diff --git a/demos/notebooks/BIDS_Matlab_04_new_datasets.m b/demos/notebooks/BIDS_Matlab_04_new_datasets.m new file mode 100644 index 00000000..b48b2f28 --- /dev/null +++ b/demos/notebooks/BIDS_Matlab_04_new_datasets.m @@ -0,0 +1,48 @@ +% # Create filenames, filepaths, and JSON +% +% (C) Copyright 2021 BIDS-MATLAB developers + +%% + +add_bids_matlab_to_path(); + +% ## Initialising a new BIDS dataset + +% This can be useful when you are going to output your analysis or your data acquisition into a new dataset. + +%% + +help bids.init; + +% Derivatives datasets have some extra info in their `dataset_description.json`. +% +% If you are going to curate the dataset with +% [Datalad](http://handbook.datalad.org/en/latest/), +% you can also mention it and this will modify the README +% to add extra info about this (taken from the datalad handbook). + +%% + +pth = fullfile(pwd, 'dummy_ds'); + +folders.subjects = {'01', '02'}; +folders.sessions = {'pre', 'post'}; +folders.modalities = {'anat', 'eeg'}; + +%% + +bids.init(pth, 'folders', folders, 'is_derivative', true, 'is_datalad_ds', true); + +%% + +!tree dummy_ds + +% Template README was generated. + +%% + +!cat dummy_ds/README + +%% + +!cat dummy_ds/dataset_description.json diff --git a/demos/notebooks/BIDS_Matlab_05_report.m b/demos/notebooks/BIDS_Matlab_05_report.m new file mode 100644 index 00000000..1e868e9d --- /dev/null +++ b/demos/notebooks/BIDS_Matlab_05_report.m @@ -0,0 +1,22 @@ +% # BIDS-matlab: reports +% +% (C) Copyright 2021 BIDS-MATLAB developers + +%% + +add_bids_matlab_to_path(); + +%% + +BIDS = bids.layout(fullfile(pwd, 'bids-examples', 'ds101')); + +%% + +read_nifti = false; +output_path = fullfile(pwd, 'output'); +verbose = true; + +bids.report(BIDS, ... + 'output_path', output_path, ... + 'read_nifti', read_nifti, ... + 'verbose', verbose); diff --git a/demos/notebooks/BIDS_Matlab_06_tsv_json.m b/demos/notebooks/BIDS_Matlab_06_tsv_json.m new file mode 100644 index 00000000..27655ab1 --- /dev/null +++ b/demos/notebooks/BIDS_Matlab_06_tsv_json.m @@ -0,0 +1,71 @@ +% # BIDS-Matlab: TSV and JSON files +% +% (C) Copyright 2021 BIDS-MATLAB developers +% +% ## Read from TSV files +% +% This can be done with the `bids.util.tsvread` function. + +%% + +add_bids_matlab_to_path(); + +%% + +BIDS = bids.layout(fullfile(pwd, 'bids-examples', 'ieeg_visual')); + +%% + +bids.query(BIDS, 'subjects'); +bids.query(BIDS, 'tasks'); +events_file = bids.query(BIDS, 'data', 'sub', '01', 'task', 'visual', 'suffix', 'events'); + +%% + +bids.util.tsvread(events_file{1}); + +% ## Write to TSV files + +%% + +tsv_file = fullfile(pwd, 'output', 'sub-01_task-STRUCTURE_events.tsv'); + +logFile.onset = [2; NaN]; +logFile.trial_type = {'motion_up'; 'static'}; +logFile.duration = [1; 4]; +logFile.speed = [NaN; 4]; +logFile.is_fixation = {'true'; '3'}; + +bids.util.tsvwrite(tsv_file, logFile); + +%% + +!cat output/sub-01_task-STRUCTURE_events.tsv + +% ## Write to JSON files + +%% + +content = struct('Name', 'test', ... + 'BIDSVersion', '1.6', ... + 'DatasetType', 'raw', ... + 'License', '', ... + 'Acknowledgements', '', ... + 'HowToAcknowledge', '', ... + 'DatasetDOI', '', ... + 'HEDVersion', '', ... + 'Funding', {{}}, ... + 'Authors', {{}}, ... + 'ReferencesAndLinks', {{}}); + +bids.util.jsonencode(fullfile(pwd, 'output', 'dataset_description.json'), content); + +%% + +!cat output/dataset_description.json + +% ## Read from JSON files + +%% + +bids.util.jsondecode(fullfile(pwd, 'output', 'dataset_description.json')); diff --git a/demos/notebooks/BIDS_Matlab_07_diagnostic.m b/demos/notebooks/BIDS_Matlab_07_diagnostic.m new file mode 100644 index 00000000..bcb79860 --- /dev/null +++ b/demos/notebooks/BIDS_Matlab_07_diagnostic.m @@ -0,0 +1,19 @@ +% # BIDS-matlab: diagnostic +% +% (C) Copyright 2021 BIDS-MATLAB developers + +%% + +add_bids_matlab_to_path(); + +%% + +BIDS = bids.layout(fullfile(pwd, 'bids-examples', 'ds000247')); + +%% + +diagnostic_table = bids.diagnostic(BIDS); + +%% + +diagnostic_table = bids.diagnostic(BIDS, 'split_by', {'task'}); diff --git a/demos/notebooks/BIDS_Matlab_08_stats_model.m b/demos/notebooks/BIDS_Matlab_08_stats_model.m new file mode 100644 index 00000000..6ae494e7 --- /dev/null +++ b/demos/notebooks/BIDS_Matlab_08_stats_model.m @@ -0,0 +1,50 @@ +% (C) Copyright 2021 BIDS-MATLAB developers + +%% + +add_bids_matlab_to_path(); + +%% + +% Create an empty model +bm = bids.Model('init', true); +filename = fullfile(pwd, 'model-empty_smdl.json'); +bm.write(filename); + +%% + +!cat model-empty_smdl.json + +%% + +% create a default bids model for a dataset +ds = fullfile(pwd, 'bids-examples', 'ds003'); +BIDS = bids.layout(ds); + +bm = bids.Model(); +bm = bm.default(BIDS); + +filename = fullfile(pwd, 'model-rhymejudgement_smdl.json'); +bm.write(filename); + +%% + +!cat model-rhymejudgement_smdl.json + +%% + +% load and query a specific model file +model_file = fullfile('..', '..', 'tests', 'data', 'model', 'model-narps_smdl.json'); +bm = bids.Model('file', model_file); + +%% + +bm.get_nodes('Level', 'Run'); + +%% + +bm.get_transformations('Level', 'Run'); +bm.get_dummy_contrasts('Level', 'Run'); +bm.get_contrasts('Level', 'Run'); +bm.get_model('Level', 'Run'); +bm.get_design_matrix('Level', 'Run'); diff --git a/examples/Makefile b/demos/notebooks/Makefile similarity index 100% rename from examples/Makefile rename to demos/notebooks/Makefile diff --git a/examples/README.md b/demos/notebooks/README.md similarity index 71% rename from examples/README.md rename to demos/notebooks/README.md index 87b064cc..f97b0236 100644 --- a/examples/README.md +++ b/demos/notebooks/README.md @@ -1,5 +1,16 @@ # README +To run some of the scripts or notebooks, you need to install the bids examples repository +in this directory + +Run the following command in the terminal: + +```bash +git clone https://github.com/bids-standard/bids-examples.git --depth 1 +``` + +# Running the notebooks with Octave + 1. Make sure that you have Octave installed. 1. If you have Conda/Jupyter/pip installed, go to step 4. Check if Conda is diff --git a/demos/notebooks/add_bids_matlab_to_path.m b/demos/notebooks/add_bids_matlab_to_path.m new file mode 100644 index 00000000..e7162863 --- /dev/null +++ b/demos/notebooks/add_bids_matlab_to_path.m @@ -0,0 +1,4 @@ +function add_bids_matlab_to_path() + % (C) Copyright 2021 BIDS-MATLAB developers + addpath(fullfile(fileparts(mfilename('fullpath')), '..', '..')); +end diff --git a/demos/notebooks/convert.py b/demos/notebooks/convert.py new file mode 100644 index 00000000..2f3617af --- /dev/null +++ b/demos/notebooks/convert.py @@ -0,0 +1,46 @@ +"""Convert Octave notebooks to scripts.""" + +import json +from pathlib import Path + +from rich import print + +this_dir = Path(__file__).parent + +notebooks = this_dir.glob("*.ipynb") + +for ntbk in notebooks: + + with open(ntbk) as f: + nb = json.load(f) + + filename = ntbk.stem.replace("-", "_") + + output_file = ntbk.with_stem(filename).with_suffix(".m") + + with open(output_file, "w") as f: + + for cell in nb["cells"]: + + if cell["cell_type"] == "markdown": + for line in cell["source"]: + print(f"% {line}", file=f, end="") + print( + "\n", + file=f, + ) + + if cell["cell_type"] == "code": + print( + "%%\n", + file=f, + ) + for line in cell["source"]: + if line.startswith("https://"): + print(f"% {line}", file=f, end="") + else: + print(f"{line}", file=f, end="") + print( + "\n", + file=f, + ) diff --git a/demos/notebooks/miss_hit.cfg b/demos/notebooks/miss_hit.cfg new file mode 100644 index 00000000..5c8d551d --- /dev/null +++ b/demos/notebooks/miss_hit.cfg @@ -0,0 +1,5 @@ +line_length: 150 + +regex_script_name: "BIDS_Matlab_[0-9]*(_[a-zA-Z]*)*" + +# exclude_dir: "bids-examples" diff --git a/demos/notebooks/test_notebooks.m b/demos/notebooks/test_notebooks.m new file mode 100644 index 00000000..a1cbab49 --- /dev/null +++ b/demos/notebooks/test_notebooks.m @@ -0,0 +1,29 @@ +function status = test_notebooks() + % run all the scripts in this directory + % (C) Copyright 2021 BIDS-MATLAB developers + + status = true; + + notebooks = dir(pwd); + + failed = []; + + for nb = 1:numel(notebooks) + if regexp(notebooks(nb).name, '^BIDS_Matlab.*m$') + fprintf(1, '\n'); + disp(notebooks(nb).name); + fprintf(1, '\n'); + try + run(notebooks(nb).name); + catch + status = false; + failed(end + 1) = nb; + end + end + end + + for f = 1:numel(failed) + warning('\n\tRunning %s failed.\n', notebooks(failed(f)).name); + end + +end diff --git a/demos/spm/extract_confounds_fmriprep.m b/demos/spm/extract_confounds_fmriprep.m new file mode 100644 index 00000000..11221797 --- /dev/null +++ b/demos/spm/extract_confounds_fmriprep.m @@ -0,0 +1,83 @@ +% extracts confounds of interest from fmriprep timeseries.tsv +% and saves them for easier ingestion by SPM model specification +% + +% (C) Copyright 2022 Remi Gau + +path_to_fmriprep = fullfile(pwd, 'fmriprep'); +output_folder = fullfile(pwd, 'spm12'); + +task_label = 'facerepetition'; +space_label = 'MNI152NLin2009cAsym'; + +% set up some regular expression to identify the confounds we want to keep +confounds_of_interest = {'^rot_[xyz]$', '^trans_[xyz]$', '^*outlier*$'}; + +% index the content of the fmriprep data set and figure out which subjects we +% have +BIDS = bids.layout(path_to_fmriprep, 'use_schema', false); +subjects = bids.query(BIDS, 'subjects'); + +% prepare the output folder structure +folders = struct('subjects', {subjects}, 'modalities', {{'stats'}}); +bids.init(output_folder, 'folders', folders); + +for i_sub = 1:numel(subjects) + + % create the filter to + filter = struct('sub', subjects{i_sub}, ... + 'task', task_label, ... + 'desc', 'confounds', ... + 'suffix', 'timeseries'); + confound_files = bids.query(BIDS, 'data', filter); + + % loop through all the desc-confounds_timeseries.tsv + % load it + % get only the condounds we need + % save it in the output folder as a mat file and a TSV + for i = 1:numel(confound_files) + + % for mat file + names = {}; + R = []; + + % for TSV + new_content = struct(); + + % load + content = bids.util.tsvread(confound_files{i}); + confounds_names = fieldnames(content); + + % create a logical vector to identify which confounds to keep + % and store confounds to keep in new variables + confounds_to_keep = regexp(confounds_names, ... + strjoin(confounds_of_interest, '|')); + confounds_to_keep = ~cellfun('isempty', confounds_to_keep); + + confounds_names = confounds_names(confounds_to_keep); + + for j = 1:numel(confounds_names) + % for mat file + names{j} = confounds_names{j}; + R(:, j) = content.(confounds_names{j}); + + % for TSV + new_content.(confounds_names{j}) = content.(confounds_names{j}); + end + + % save to mat and TSV + output_file_name = bids.File(confound_files{i}); + output_file_name.entities.desc = ''; + output_file_name.suffix = 'confounds'; + + output_file_name.path = fullfile(output_folder, ['sub-' subjects{i_sub}], 'stats'); + + bids.util.tsvwrite(fullfile(output_file_name.path, output_file_name.filename), ... + new_content); + + output_file_name.extension = '.mat'; + save(fullfile(output_file_name.path, output_file_name.filename), 'names', 'R'); + + end + +end diff --git a/demos/spm/facerep/code/convert_facerep_ds.m b/demos/spm/facerep/code/convert_facerep_ds.m new file mode 100644 index 00000000..c16777ea --- /dev/null +++ b/demos/spm/facerep/code/convert_facerep_ds.m @@ -0,0 +1,385 @@ +function output_dir = convert_facerep_ds(input_dir, output_dir) + % + % downloads the face repetition dataset from SPM and convert it to BIDS + % + % requires BIDS matlab + % + % Adapted from its counterpart for MoAE + % + % + + % (C) Copyright 2021 Remi Gau + + % TODO + % recode the lag column so that the identity of each stimulus is captured + % in another column and lag can be recomputed + + if nargin < 1 + input_dir = fullfile(fileparts(mfilename('fullpath')), '..', 'sourcedata'); + end + + if nargin < 2 + output_dir = fullfile(input_dir, '..'); + end + + subject = 'sub-01'; + task_name = 'face repetition'; + nb_slices = 24; + repetition_time = 2; + echo_time = 0.04; + opt.indent = ' '; + + %% Create output folder structure + spm_mkdir(output_dir, subject, {'anat', 'func'}); + + %% Structural MRI + anat_hdr = spm_vol(fullfile(input_dir, 'Structural', 'sM03953_0007.img')); + anat_data = spm_read_vols(anat_hdr); + 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(input_dir, 'RawEPI'), '^sM.*\.img$'); + spm_file_merge(func_files, ... + fullfile(output_dir, 'sub-01', 'func', ... + ['sub-01_task-' strrep(task_name, ' ', '') '_bold.nii'])); + delete(fullfile(output_dir, 'sub-01', 'func', ... + ['sub-01_task-' strrep(task_name, ' ', '') '_bold.mat'])); + + %% And everything else + create_events_tsv_file(input_dir, output_dir, task_name, repetition_time); + create_events_json(output_dir, task_name, opt); + 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(input_dir, output_dir, task_name, repetition_time) + + load(fullfile(input_dir, 'all_conditions.mat'), ... + 'names', 'onsets', 'durations'); + + load(fullfile(input_dir, 'sots.mat'), ... + 'itemlag'); + + % fill in with zeros as we can't have empty cells in tsv files + itemlag{1} = nan(size(onsets{1}))'; + itemlag{3} = nan(size(onsets{3}))'; + + onset_column = []; + duration_column = []; + trial_type_column = []; + lag_column = []; + + for iCondition = 1:numel(names) + onset_column = [onset_column; onsets{iCondition}]; %#ok<*USENS> + duration_column = [duration_column; durations{iCondition}']; %#ok<*AGROW> + trial_type_column = [trial_type_column; repmat(names{iCondition}, ... + size(onsets{iCondition}, 1), 1)]; + lag_column = [lag_column, itemlag{iCondition}]; + end + + lag_column = lag_column'; + + event_type_column = repmat('show_face', size(onset_column)); + + repetition_type_column = []; + face_type_column = {}; + + % Build 2 columns: + % - one for face type (famous, unfamiliar) + % - one for 1rst and 2nd presentation + % to make the the 2X2 design more explicit in the TSV and be able to have + % single TSV for parametric and non parametric models + for i = 1:size(trial_type_column, 1) + + if strcmp(trial_type_column(i, 2), '1') + repetition_type_column(i, 1) = 1; + else + repetition_type_column(i, 1) = 2; + end + + if strcmp(trial_type_column(i, 1), 'N') + face_type_column{i, 1} = 'unfamiliar'; + else + face_type_column{i, 1} = 'famous'; + end + + end + + % sort trials by their presentation time + [onset_column, idx] = sort(onset_column); + duration_column = duration_column(idx); + lag_column = lag_column(idx, :); + repetition_type_column = repetition_type_column(idx, :); + face_type_column = face_type_column(idx, :); + + onset_column = repetition_time * onset_column; + + tsv_content = struct('onset', onset_column, ... + 'duration', duration_column, ... + 'event_type', {cellstr(event_type_column)}, .... + 'repetition_type', repetition_type_column, ... + 'trial_type', {cellstr(face_type_column)}, ... + 'lag', lag_column); + + spm_save(fullfile(output_dir, 'sub-01', 'func', ... + ['sub-01_task-' strrep(task_name, ' ', '') '_events.tsv']), ... + tsv_content); + +end + +function create_events_json(output_dir, task_name, opt) + + onset_desc = ['Onset of the event measured from the beginning ' ... + 'of the acquisition of the first volume ', ... + 'in the corresponding task imaging data file.']; + + lag_desc = ['the Lags code, for each second presentation of a face, ', ... + 'the number of trials intervening between this (repeated) presentation ', ... + 'and its previous (first) presentation.', ... + 'A value of 0 means this is the first presentation.']; + + repetition_type_desc = 'Factor indicating whether this image has been already seen.'; + first_show_desc = 'indicates the first display of this face.'; + delayed_repeat_desc = 'indicates face was seen X trials ago.'; + + face_type_desc = 'Factor indicating type of face image being displayed.'; + famous_face_desc = 'A face that should be recognized by the participants.'; + unfamiliar_face_desc = 'A face that should not be recognized by the participants.'; + + event_type_HED = struct('show_face', ... + 'Sensory-event, Experimental-stimulus, (Def/Face-image, Onset)'); + + repetition_type_levels = struct('first_show', first_show_desc, ... + 'delayed_repeat', delayed_repeat_desc); + repetition_type_HED = struct('first_show', 'Def/First-show-cond', ... + 'delayed_repeat', 'Def/Delayed-repeat-cond'); + + face_type_levels = struct( ... + 'famous_face', famous_face_desc, ... + 'unfamiliar_face', unfamiliar_face_desc); + face_type_HED = struct( ... + 'famous_face', 'Def/Famous-face-cond', ... + 'unfamiliar_face', 'Def/Unfamiliar-face-cond'); + + hed_face_image_def = ['(Definition/Face-image, ', ... + '(Visual, (Foreground-view, ', ... + '((Image, Face, Hair), Color/Grayscale), ', ... + '((White, Cross), (Center-of, Screen))), ', ... + '(Background-view, Black), ', ... + 'Description/', ... + 'A happy or neutral face in frontal or 3/4 frontal pose ', ... + 'with long hair ', ... + 'cropped presented as an achromatic foreground image ', ... + 'on a black background ', ... + 'with a white fixation cross superposed.)', ... + ')']; + hed_def_sensory = struct( ... + 'Description', 'Dictionary for gathering sensory definitions', ... + 'HED', struct('Face_image_def', hed_face_image_def)); + + % pattern to use to create condition HED definitions + cdt_hed_def = '(Definition/%s, (Condition-variable, Label/%s, %s, Description/%s))'; + create_def = @(x) sprintf(cdt_hed_def, x.def, x.label, x.tags, x.desc); + + label = 'Face-type'; + + famous.def = 'Famous-face-cond'; + famous.label = label; + famous.tags = '(Image, (Face, Famous))'; + famous.desc = famous_face_desc; + + unfamiliar.def = 'Unfamiliar-face-cond'; + unfamiliar.label = label; + unfamiliar.tags = '(Image, (Face, Unfamiliar))'; + unfamiliar.desc = unfamiliar_face_desc; + + famous_def = create_def(famous); + unfamiliar_def = create_def(unfamiliar); + + label = 'Repetition-type'; + + first_show.def = 'First-show-cond'; + first_show.label = label; + first_show.tags = '(First-item, Repetition-number/1)'; + first_show.desc = first_show_desc; + + delayed_repeat.def = 'Delayed-repeat-cond'; + delayed_repeat.label = label; + delayed_repeat.tags = '(Later-item, Repetition-number/2)'; + delayed_repeat.desc = delayed_repeat_desc; + + first_show_def = create_def(first_show); + delayed_repeat_def = create_def(delayed_repeat); + + hed_def_conds = struct( ... + 'Description', ... + 'Dictionary for gathering experimental condition definitions', ... + 'HED', struct( ... + 'Famous_face_cond_def', famous_def, ... + 'Unfamiliar_face_cond_def', unfamiliar_def, ... + 'First_show_cond_def', first_show_def, ... + 'Delayed_repeat_cond_def', delayed_repeat_def)); + + content = struct( ... + 'onset', struct('Description', onset_desc), ... + 'duration', struct('Description', ... + 'Duration of the event (measured from onset).', ... + 'units', 'seconds'), ... + 'trial_type', struct('Description', ... + ['Primary categorisation of each trial to identify', ... + ' them as instances of the experimental conditions.']), ... + 'lag', struct('Description', lag_desc), ... + 'event_type', struct('LongName', 'Event category', ... + 'Description', 'The main category of the event.', ... + 'Levels', struct('show_face', ... + ['Display a face to mark', ... + ' end of pre-stimulus and' ... + ' start of blink-inhibition.']), ... + 'HED', event_type_HED), ... + 'repetition_type', struct( ... + 'Description', repetition_type_desc, ... + 'Levels', repetition_type_levels, ... + 'HED', repetition_type_HED), ... + 'face_type', struct( ... + 'Description', face_type_desc, ... + 'Levels', face_type_levels, ... + 'HED', face_type_HED), ... + 'hed_def_sensory', hed_def_sensory, ... + 'hed_def_conds', hed_def_conds ... + ); + + spm_save(fullfile(output_dir, ['task-', strrep(task_name, ' ', ''), '_events.json']), ... + content, ... + opt); + +end + +function create_readme(output_dir) + + rdm = { + ' ___ ____ __ __' + '/ __)( _ \( \/ ) Statistical Parametric Mapping' + '\__ \ )___/ ) ( Wellcome Centre for Human Neuroimaging' + '(___/(__) (_/\/\_) https://www.fil.ion.ucl.ac.uk/spm/' + + '' + ' Face repetition example event-related fMRI dataset' + '________________________________________________________________________' + '' + '???' + '' + 'Summary:' + '7 Files, 79.32MB' + '1 - Subject' + '1 - Session' + '' + 'Available Tasks:' + 'face repetition' + '' + 'Available Modalities:' + 'T1w' + 'bold' + 'events' + '' + 'These whole brain BOLD/EPI images were acquired on a modified ???T' + 'Siemens MAGNETOM Vision system.' + '351 acquisitions were made.' + 'Each EPI acquisition consisted of 24 descending slices:' + '- matrix size: 64x64' + '- voxel size: 3mm x 3mm x 3mm with 1.5mm gap' + '- repetition time: 2s' + '- echo time: 40ms' + '' + 'Experimental design:' + '- 2x2 factorial event-related fMRI' + '- One session (one subject)' + '- (Famous vs. Nonfamous) x (1st vs 2nd presentation) of faces ' + ' against baseline of checkerboard' + '- 2 presentations of 26 Famous and 26 Nonfamous Greyscale photographs, ' + ' for 0.5s, randomly intermixed, for fame judgment task ' + ' (one of two right finger key presses).' + '- Parameteric factor "lag" = number of faces intervening ' + ' between repetition of a specific face + 1' + '- Minimal SOA=4.5s, with probability 2/3 (ie 1/3 null events)' + '' + 'A structural image was also acquired.'}; + + % TODO + % use spm_save to actually write this file? + fid = fopen(fullfile(output_dir, 'README'), 'wt'); + for i = 1:numel(rdm) + fprintf(fid, '%s\n', rdm{i}); + end + fclose(fid); + +end + +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(output_dir, 'CHANGES'), 'wt'); + + for i = 1:numel(cg) + fprintf(fid, '%s\n', cg{i}); + end + fclose(fid); + +end + +function create_datasetdescription(output_dir, opt) + + descr = struct( ... + 'BIDSVersion', '1.4.0', ... + 'Name', 'Mother of All Experiments', ... + 'Authors', {{ ... + 'Henson, R.N.A.', ... + 'Shallice, T.', ... + 'Gorno-Tempini, M.-L.', ... + 'Dolan, R.J.'}}, ... + 'ReferencesAndLinks', ... + {{'https://www.fil.ion.ucl.ac.uk/spm/data/face_rep/', ... + ['Henson, R.N.A., Shallice, T., Gorno-Tempini, M.-L. ' ... + 'and Dolan, R.J. (2002),', ... + 'Face repetition effects in implicit and explicit memory tests as', ... + 'measured by fMRI. Cerebral Cortex, 12, 178-186.'], ... + 'doi:10.1093/cercor/12.2.178'}} ... + ); + + spm_save(fullfile(output_dir, 'dataset_description.json'), ... + descr, ... + opt); + +end + +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); + + task = struct( ... + 'RepetitionTime', repetition_time, ... + 'EchoTime', echo_time, ... + 'SliceTiming', slice_timing, ... + 'NumberOfVolumesDiscardedByScanner', 0, ... + 'NumberOfVolumesDiscardedByUser', 0, ... + 'TaskName', task_name, ... + 'TaskDescription', ... + ['2 presentations of 26 Famous and 26 Nonfamous Greyscale photographs, ', ... + 'for 0.5s, randomly intermixed, for fame judgment task ', ... + '(one of two right finger key presses).'], ... + 'Manufacturer', 'Siemens', ... + 'ManufacturersModelName', 'MAGNETOM Vision', ... + 'MagneticFieldStrength', 2); + + spm_save(fullfile(output_dir, ... + ['task-' strrep(task_name, ' ', '') '_bold.json']), ... + task, ... + opt); + +end diff --git a/demos/spm/facerep/code/spm_facerep.m b/demos/spm/facerep/code/spm_facerep.m new file mode 100644 index 00000000..991d66f5 --- /dev/null +++ b/demos/spm/facerep/code/spm_facerep.m @@ -0,0 +1,372 @@ +% (C) Copyright 2021 Remi Gau +% (C) Copyright 2014 Guillaume Flandin, Wellcome Centre for Human Neuroimaging + +% TODO compare results to original script +% TODO: +% fix model specification warning + +clear; + +download = true; +force = true; +verbose = true; +use_schema = true; +tolerant = false; + +preproc_pipeline_name = 'spm12-preproc'; +stats_pipeline_name = 'spm12-stats'; + +subject_label = '01'; + +%% Download + +% clean previously failed download +if isdir(fullfile(pwd, '..', 'face_rep')) + rmdir(fullfile(pwd, '..', 'face_rep'), 's'); +end + +if download + pth = bids.util.download_ds('source', 'spm', ... + 'demo', 'facerep', ... + 'force', false, ... + 'verbose', verbose, ... + 'out_path', fullfile(pwd, '..', 'sourcedata')); +else + pth = fullfile(pwd, '..', 'raw'); +end +%% Move file into source folder +source_path = fullfile(pwd, '..', 'sourcedata'); +raw_path = fullfile(pwd, '..', 'raw'); +derivatives_pth = fullfile(pth, 'derivatives'); + +%% CONVERT TO BIDS +pth = convert_facerep_ds(source_path, raw_path); + +%% COPY TO DERIVATIVES +BIDS = bids.layout(pth, ... + 'use_schema', use_schema, ... + 'tolerant', tolerant, ... + 'verbose', verbose); + +% copy the dataset into a folder for derivatives +bids.copy_to_derivative(BIDS, ... + 'pipeline_name', preproc_pipeline_name, ... + 'out_path', fullfile(pth, 'derivatives'), ... + 'unzip', true, ... + 'force', force, ... + 'skip_dep', false, ... + 'verbose', verbose); + +% prepare folder for stats +stats_pth = fullfile(derivatives_pth, stats_pipeline_name); +folders = struct('subjects', {{subject_label}}, ... + 'modalities', {{'statsCategorical'}}); +is_derivative = true; +bids.init(stats_pth, 'folders', folders, 'is_derivative', is_derivative); + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% WRITE REPORT + +% read the dataset +BIDS = bids.layout(fullfile(derivatives_pth, preproc_pipeline_name), ... + 'use_schema', use_schema, ... + 'tolerant', tolerant, ... + 'verbose', verbose); + +log_folder = fullfile(BIDS.pth, 'log'); +mkdir(log_folder); +bids.report(BIDS, ... + 'output_path', log_folder, ... + 'read_nifti', true, ... + 'verbose', verbose); + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% The rest of the batch script was adapted from that of the SPM website: +% http://www.fil.ion.ucl.ac.uk/spm/data/face_rep/ +% as described in the SPM manual: +% http://www.fil.ion.ucl.ac.uk/spm/doc/spm12_manual.pdf#Chap:data:faces +% __________________________________________________________________________ +% Copyright (C) 2014-2015 Wellcome Trust Centre for Neuroimaging + +% Guillaume Flandin +% $Id: face_rep_spm12_batch.m 17 2015-03-06 11:24:19Z guillaume $ + +% Initialise SPM +% -------------------------------------------------------------------------- +spm('Defaults', 'fMRI'); +spm_jobman('initcfg'); + +% Change working directory (useful for PostScript (.ps) files only) +% -------------------------------------------------------------------------- +% clear matlabbatch +% matlabbatch{1}.cfg_basicio.file_dir.dir_ops.cfg_cd.dir = cellstr(data_path); +% spm_jobman('run',matlabbatch); + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% SPATIAL PREPROCESSING + +clear matlabbatch; + +% Select functional and structural scans +% -------------------------------------------------------------------------- +a = bids.query(BIDS, 'data', ... + 'sub', subject_label, ... + 'suffix', 'T1w'); +f = bids.query(BIDS, 'data', ... + 'sub', subject_label, ... + 'suffix', 'bold'); +metadata = bids.query(BIDS, 'metadata', ... + 'sub', subject_label, ... + 'suffix', 'bold'); + +% Realign +% -------------------------------------------------------------------------- +matlabbatch{1}.spm.spatial.realign.estwrite.data{1} = cellstr(f); + +% Slice Timing Correction +% -------------------------------------------------------------------------- +temporal.st.scans{1} = cellstr(spm_file(f, 'prefix', 'r')); +temporal.st.nslices = numel(metadata.SliceTiming); +temporal.st.tr = metadata.RepetitionTime; +temporal.st.ta = metadata.RepetitionTime - metadata.RepetitionTime / numel(metadata.SliceTiming); +temporal.st.so = metadata.SliceTiming / 1000; +temporal.st.refslice = median(metadata.SliceTiming); + +matlabbatch{2}.spm.temporal = temporal; + +% Coregister +% -------------------------------------------------------------------------- +coreg.estimate.ref = cellstr(spm_file(f, 'prefix', 'mean')); +coreg.estimate.source = cellstr(a); +matlabbatch{3}.spm.spatial.coreg = coreg; + +% Segment +% -------------------------------------------------------------------------- +preproc.channel.vols = cellstr(a); +preproc.channel.write = [0 1]; +preproc.warp.write = [0 1]; +matlabbatch{4}.spm.spatial.preproc = preproc; + +% Normalise: Write +% -------------------------------------------------------------------------- +normalise.write.subj.def = cellstr(spm_file(a, 'prefix', 'y_', 'ext', 'nii')); +normalise.write.subj.resample = cellstr(char(spm_file(f{1}, 'prefix', 'ar'), ... + spm_file(f{1}, 'prefix', 'mean'))); +normalise.write.woptions.vox = [3 3 3]; + +matlabbatch{5}.spm.spatial.normalise = normalise; + +normalise.write.subj.def = cellstr(spm_file(a, 'prefix', 'y_', 'ext', 'nii')); +normalise.write.subj.resample = cellstr(spm_file(a, 'prefix', 'm', 'ext', 'nii')); +normalise.write.woptions.vox = [1 1 1.5]; + +matlabbatch{6}.spm.spatial.normalise = normalise; + +% Smooth +% -------------------------------------------------------------------------- +smooth.data = cellstr(spm_file(f, 'prefix', 'war')); +smooth.fwhm = [8 8 8]; + +matlabbatch{7}.spm.spatial.smooth = smooth; + +save(fullfile(pwd, 'face_batch_preprocessing.mat'), 'matlabbatch'); + +spm_jobman('run', matlabbatch); + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% CLASSICAL STATISTICAL ANALYSIS (CATEGORICAL) + +% re-read the dataset without the schela to index the new files +BIDS = bids.layout(BIDS.pth, 'use_schema', false); + +f = bids.query(BIDS, 'data', ... + 'sub', subject_label, ... + 'suffix', 'bold', 'prefix', 'swar'); + +realignment = bids.query(BIDS, 'data', ... + 'sub', subject_label, ... + 'suffix', 'bold', 'prefix', 'rp_'); + +events = bids.query(BIDS, 'data', ... + 'sub', subject_label, ... + 'suffix', 'events', ... + 'extension', '.tsv'); + +events = bids.util.tsvread(events{1}); + +data_path = fullfile(stats_pth, ['sub-' subject_label]); +subj_stats_pth = fullfile(data_path, 'statsCategorical'); +SPM_mat = fullfile(subj_stats_pth, 'SPM.mat'); + +clear matlabbatch; + +%% Load and prepare onsets +% -------------------------------------------------------------------------- +onsets = events.onset; + +face_type = unique(events.face_type); +repetition_type = unique(events.repetition_type); + +% Model Specification +% -------------------------------------------------------------------------- +fmri_spec.dir = cellstr(subj_stats_pth); +fmri_spec.timing.units = 'secs'; +fmri_spec.timing.RT = metadata.RepetitionTime; +fmri_spec.timing.fmri_t = metadata.SliceTiming; +fmri_spec.timing.fmri_t0 = median(1:numel(metadata.SliceTiming)); +fmri_spec.sess.scans = cellstr(f); + +condition_nb = 1; + +for i = 1:numel(face_type) + for j = 1:numel(repetition_type) + + is_this_face_type = ismember(events.face_type, face_type{i}); + is_this_repetition_type = ismember(events.repetition_type, repetition_type(j)); + idx = all([is_this_face_type, is_this_repetition_type], 2); + + fmri_spec.sess.cond(condition_nb).name = [face_type{i} '_' num2str(repetition_type(j))]; + fmri_spec.sess.cond(condition_nb).onset = onsets(idx); + fmri_spec.sess.cond(condition_nb).duration = 0; + + condition_nb = condition_nb + 1; + + end +end + +fmri_spec.sess.multi_reg = cellstr(realignment); +fmri_spec.fact(1).name = 'Fame'; +fmri_spec.fact(1).levels = 2; +fmri_spec.fact(2).name = 'Rep'; +fmri_spec.fact(2).levels = 2; +fmri_spec.bases.hrf.derivs = [1 1]; + +matlabbatch{1}.spm.stats.fmri_spec = fmri_spec; + +% Model Estimation +% -------------------------------------------------------------------------- +matlabbatch{2}.spm.stats.fmri_est.spmmat = cellstr(SPM_mat); + +save(fullfile(pwd, 'categorical_spec.mat'), 'matlabbatch'); + +% Inference +% -------------------------------------------------------------------------- +results.spmmat = cellstr(SPM_mat); +results.conspec.contrasts = Inf; +results.conspec.threshdesc = 'FWE'; + +matlabbatch{3}.spm.stats.results = results; + +results.spmmat = cellstr(SPM_mat); +results.conspec.contrasts = 3; +results.conspec.threshdesc = 'none'; +results.conspec.thresh = 0.001; +results.conspec.extent = 0; +results.conspec.mask.contrasts = 5; +results.conspec.mask.mtype = 0; +results.conspec.mask.thresh = 0.001; + +matlabbatch{4}.spm.stats.results = results; + +con.spmmat = cellstr(SPM_mat); +con.consess{1}.fcon.name = 'Movement-related effects'; +con.consess{1}.fcon.weights = [zeros(6, 3 * 4) eye(6)]; + +matlabbatch{5}.spm.stats.con = con; + +results.spmmat = cellstr(SPM_mat); +results.conspec.contrasts = 17; +results.conspec.threshdesc = 'FWE'; + +matlabbatch{6}.spm.stats.results = results; + +% Run +% -------------------------------------------------------------------------- +save(fullfile(pwd, 'face_batch_categorical.mat'), 'matlabbatch'); +spm_jobman('run', matlabbatch); + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% BIDS DATASET NOT YET READY FOR THIS +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% CLASSICAL STATISTICAL ANALYSIS (PARAMETRIC) + +clear matlabbatch; + +% Load lags +% -------------------------------------------------------------------------- +lag = events.lag; + +% Output directory +% -------------------------------------------------------------------------- +cfg_mkdir.parent = cellstr(fullfile(subj_stats_pth, '..')); +cfg_mkdir.name = 'parametric'; + +matlabbatch{1}.cfg_basicio.file_dir.dir_ops.cfg_mkdir = cfg_mkdir; + +% Model Specification (copy and edit the categorical one) +% -------------------------------------------------------------------------- +batch_categ = load(fullfile(pwd, 'categorical_spec.mat')); +matlabbatch{2} = batch_categ.matlabbatch{1}; + +condition_nb = 1; + +for i = 1:numel(face_type) + for j = 1:numel(repetition_type) + + is_this_face_type = ismember(events.face_type, face_type{i}); + is_this_repetition_type = ismember(events.repetition_type, repetition_type(j)); + idx = all([is_this_face_type, is_this_repetition_type], 2); + + if ismember(condition_nb, [2 4]) + pmod.name = 'Lag'; + pmod.param = lag(idx); + pmod.poly = 2; + matlabbatch{2}.spm.stats.fmri_spec.sess.cond(condition_nb).pmod = pmod; + else + matlabbatch{2}.spm.stats.fmri_spec.sess.cond(condition_nb).pmod = struct('name', {}, ... + 'param', {}, ... + 'poly', {}); + end + + condition_nb = condition_nb + 1; + + end +end + +matlabbatch{2}.spm.stats.fmri_spec.timing.units = 'secs'; +matlabbatch{2}.spm.stats.fmri_spec.timing.RT = metadata.RepetitionTime; +matlabbatch{2}.spm.stats.fmri_spec.timing.fmri_t = metadata.SliceTiming; +matlabbatch{2}.spm.stats.fmri_spec.timing.fmri_t0 = median(1:numel(metadata.SliceTiming)); +matlabbatch{2}.spm.stats.fmri_spec.sess.scans = cellstr(f); + +matlabbatch{2}.spm.stats.fmri_spec.dir = cellstr(fullfile(data_path, 'parametric')); +matlabbatch{2}.spm.stats.fmri_spec.bases.hrf.derivs = [0 0]; + +% Model Estimation +% -------------------------------------------------------------------------- +matlabbatch{3}.spm.stats.fmri_est.spmmat = cellstr(fullfile(data_path, 'parametric', 'SPM.mat')); + +% Inference +% -------------------------------------------------------------------------- +matlabbatch{4}.spm.stats.con.spmmat = cellstr(fullfile(data_path, 'parametric', 'SPM.mat')); +matlabbatch{4}.spm.stats.con.consess{1}.fcon.name = 'Famous Lag'; +matlabbatch{4}.spm.stats.con.consess{1}.fcon.weights = [zeros(2, 6) eye(2)]; + +matlabbatch{5}.spm.stats.results.spmmat = cellstr(fullfile(data_path, 'parametric', 'SPM.mat')); +matlabbatch{5}.spm.stats.results.conspec.contrasts = Inf; +matlabbatch{5}.spm.stats.results.conspec.threshdesc = 'FWE'; + +matlabbatch{6}.spm.stats.results.spmmat = cellstr(fullfile(data_path, 'parametric', 'SPM.mat')); +matlabbatch{6}.spm.stats.results.conspec.contrasts = 9; +matlabbatch{6}.spm.stats.results.conspec.threshdesc = 'none'; +matlabbatch{6}.spm.stats.results.conspec.thresh = 0.001; +matlabbatch{6}.spm.stats.results.conspec.extent = 0; +matlabbatch{6}.spm.stats.results.conspec.mask.contrasts = 5; +matlabbatch{6}.spm.stats.results.conspec.mask.thresh = 0.05; +matlabbatch{6}.spm.stats.results.conspec.mask.mtype = 0; + +% Run +% -------------------------------------------------------------------------- +save('face_batch_parametric.mat', 'matlabbatch'); +spm_jobman('interactive', matlabbatch); +% spm_jobman('run', matlabbatch); diff --git a/demos/spm/moae/code/spm_moae.m b/demos/spm/moae/code/spm_moae.m new file mode 100644 index 00000000..b4c15a78 --- /dev/null +++ b/demos/spm/moae/code/spm_moae.m @@ -0,0 +1,216 @@ +% (C) Copyright 2021 Remi Gau +% (C) Copyright 2014 Guillaume Flandin, Wellcome Centre for Human Neuroimaging + +clear; + +force = true; +verbose = true; +use_schema = true; +tolerant = false; + +preproc_pipeline_name = 'spm12_preproc'; +stats_pipeline_name = 'spm12_stats'; + +subject_label = '01'; + +%% +pth = bids.util.download_ds('source', 'spm', ... + 'demo', 'moae', ... + 'force', force, ... + 'verbose', verbose); + +derivatives_pth = fullfile(pth, 'derivatives'); + +%% COPY DATA TO DERIVATIVES FOLDER +BIDS = bids.layout(pth, ... + 'use_schema', use_schema, ... + 'tolerant', tolerant, ... + 'verbose', verbose); + +% copy the dataset into a folder for derivatives +bids.copy_to_derivative(BIDS, ... + 'pipeline_name', preproc_pipeline_name, ... + 'out_path', fullfile(pth, 'derivatives'), ... + 'unzip', true, ... + 'force', force, ... + 'skip_dep', false, ... + 'verbose', verbose); + +% prepare folder for stats +stats_pth = fullfile(derivatives_pth, stats_pipeline_name); +folders = struct('subjects', {{subject_label}}, ... + 'modalities', {{'stats'}}); +is_derivative = true; +bids.init(stats_pth, 'folders', folders, 'is_derivative', is_derivative); + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% WRITE DATASET DESCRIPTION REPORT + +% read the dataset +BIDS = bids.layout(fullfile(pth, 'derivatives', preproc_pipeline_name), ... + 'use_schema', use_schema, ... + 'tolerant', tolerant, ... + 'verbose', verbose); + +% write the report in the log folder +mkdir(BIDS.pth, 'log'); +bids.report(BIDS, ... + 'output_path', fullfile(pth, 'derivatives', preproc_pipeline_name, 'log'), ... + 'read_nifti', true, ... + 'verbose', verbose); + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% The rest of the batch script was adapted from that of the SPM website: +% http://www.fil.ion.ucl.ac.uk/spm/data/auditory/ +% as described in the SPM manual: +% http://www.fil.ion.ucl.ac.uk/spm/doc/manual.pdf#Chap:data:auditory +% __________________________________________________________________________ +% Copyright (C) 2014 Wellcome Trust Centre for Neuroimaging + +% Guillaume Flandin +% $Id: auditory_spm12_batch.m 8 2014-09-29 18:11:56Z guillaume $ + +%% Initialise SPM +% -------------------------------------------------------------------------- +spm('Defaults', 'fMRI'); +spm_jobman('initcfg'); + +clear matlabbatch; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% SPATIAL PREPROCESSING + +anat = bids.query(BIDS, 'data', ... + 'sub', subject_label, ... + 'suffix', 'T1w'); +func = bids.query(BIDS, 'data', ... + 'sub', subject_label, ... + 'suffix', 'bold'); + +% Realign +% -------------------------------------------------------------------------- +estwrite.data{1} = cellstr(func); +estwrite.roptions.which = [0 1]; + +matlabbatch{1}.spm.spatial.realign.estwrite = estwrite; + +% Coregister +% -------------------------------------------------------------------------- +estimate.ref = cellstr(spm_file(func, 'prefix', 'mean')); +estimate.source = cellstr(anat); + +matlabbatch{2}.spm.spatial.coreg.estimate = estimate; + +% Segment +% -------------------------------------------------------------------------- +preproc.channel.vols = cellstr(anat); +preproc.channel.write = [0 1]; +preproc.warp.write = [0 1]; + +matlabbatch{3}.spm.spatial.preproc = preproc; + +% Normalise: Write +% -------------------------------------------------------------------------- +write.subj.def = cellstr(spm_file(anat, 'prefix', 'y_', ... + 'ext', 'nii')); +write.subj.resample = cellstr(func); +write.woptions.vox = [3 3 3]; + +matlabbatch{4}.spm.spatial.normalise.write = write; + +write.subj.def = cellstr(spm_file(anat, 'prefix', 'y_', ... + 'ext', 'nii')); +write.subj.resample = cellstr(spm_file(anat, 'prefix', 'm', ... + 'ext', 'nii')); +write.woptions.vox = [1 1 3]; + +matlabbatch{5}.spm.spatial.normalise.write = write; + +% Smooth +% -------------------------------------------------------------------------- +smooth.data = cellstr(spm_file(func, 'prefix', 'w')); +smooth.fwhm = [6 6 6]; +matlabbatch{6}.spm.spatial.smooth = smooth; + +spm_jobman('run', matlabbatch); + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% GLM SPECIFICATION, ESTIMATION, INFERENCE, RESULTS + +% re-read the dataset without the schela to index the new files +use_schema = false; +BIDS = bids.layout(fullfile(pth, 'derivatives', preproc_pipeline_name), ... + 'use_schema', use_schema); + +func = bids.query(BIDS, 'data', ... + 'sub', subject_label, ... + 'suffix', 'bold', ... + 'prefix', 'sw'); + +metadata = bids.query(BIDS, 'metadata', ... + 'sub', subject_label, ... + 'suffix', 'bold', ... + 'prefix', 'sw'); + +events = bids.query(BIDS, 'data', ... + 'sub', subject_label, ... + 'suffix', 'events', ... + 'extension', '.tsv'); + +events = spm_load(events{1}); + +subj_stats_pth = fullfile(BIDS.pth, ['sub-' subject_label], 'stats'); +SPM_mat = fullfile(subj_stats_pth, 'SPM.mat'); + +clear matlabbatch; + +% Model Specification +% -------------------------------------------------------------------------- +fmri_spec.dir = cellstr(subj_stats_pth); +fmri_spec.timing.units = 'secs'; +fmri_spec.timing.RT = metadata.RepetitionTime; +fmri_spec.sess.scans = cellstr(func); +fmri_spec.sess.cond.name = 'active'; +fmri_spec.sess.cond.onset = events.onset; +fmri_spec.sess.cond.duration = events.duration; + +matlabbatch{1}.spm.stats.fmri_spec = fmri_spec; + +% Model Estimation +% -------------------------------------------------------------------------- +matlabbatch{2}.spm.stats.fmri_est.spmmat = cellstr(SPM_mat); + +% Contrasts +% -------------------------------------------------------------------------- +con.spmmat = cellstr(SPM_mat); +con.consess{1}.tcon.name = 'Listening > Rest'; +con.consess{1}.tcon.weights = [1 0]; +con.consess{2}.tcon.name = 'Rest > Listening'; +con.consess{2}.tcon.weights = [-1 0]; + +matlabbatch{3}.spm.stats.con = con; + +% Inference Results +% -------------------------------------------------------------------------- +results.spmmat = cellstr(SPM_mat); +results.conspec.contrasts = 1; +results.conspec.threshdesc = 'FWE'; +results.conspec.thresh = 0.05; +results.conspec.extent = 0; +results.print = false; + +matlabbatch{4}.spm.stats.results = results; + +% Rendering +% -------------------------------------------------------------------------- +surface = fullfile(spm('Dir'), 'canonical', 'cortex_20484.surf.gii'); +display.rendfile = {surface}; +display.conspec.spmmat = cellstr(SPM_mat); +display.conspec.contrasts = 1; +display.conspec.threshdesc = 'FWE'; +display.conspec.thresh = 0.05; +display.conspec.extent = 0; + +matlabbatch{5}.spm.util.render.display = display; + +spm_jobman('run', matlabbatch); diff --git a/demos/transformers/data/sub-03_task-VisuoTact_run-02_events.tsv b/demos/transformers/data/sub-03_task-VisuoTact_run-02_events.tsv new file mode 100644 index 00000000..5886795c --- /dev/null +++ b/demos/transformers/data/sub-03_task-VisuoTact_run-02_events.tsv @@ -0,0 +1,175 @@ +onset duration trial_type expected_response given_response response_time + 0.000 1.358 BASE 0 0 0.000 + 1.358 2.026 CONG_LEFT 0 0 0.000 + 3.384 1.714 ISI 0 0 0.000 + 5.098 2.026 REST 0 0 0.000 + 7.124 2.047 ISI 0 0 0.000 + 9.171 2.046 P_LEFT 0 0 0.000 + 11.217 1.893 ISI 0 0 0.000 + 13.110 1.712 RESPONSE_P_LEFT 1 0 2147483.647 + 14.822 1.693 ISI 0 0 0.000 + 16.515 2.046 REST 0 0 0.000 + 18.561 2.381 ISI 0 0 0.000 + 20.942 2.028 INCONG_VR_PL 0 0 0.000 + 22.970 2.314 ISI 0 0 0.000 + 25.284 2.047 CONG_RIGHT 0 0 0.000 + 27.331 1.959 ISI 0 0 0.000 + 29.290 2.047 INCONG_VL_PR 0 0 0.000 + 31.337 1.712 ISI 0 0 0.000 + 33.049 2.027 V_LEFT 0 0 0.000 + 35.076 2.180 ISI 0 0 0.000 + 37.256 1.692 RESPONSE_V_LEFT 1 0 2147483.647 + 38.948 1.914 ISI 0 0 0.000 + 40.862 2.047 P_RIGHT 0 0 0.000 + 42.909 2.294 ISI 0 0 0.000 + 45.203 2.047 V_RIGHT 0 0 0.000 + 47.250 2.381 ISI 0 0 0.000 + 49.631 2.046 P_LEFT 0 0 0.000 + 51.677 1.693 ISI 0 0 0.000 + 53.370 2.046 INCONG_VL_PR 0 0 0.000 + 55.416 1.961 ISI 0 0 0.000 + 57.377 1.712 RESPONSE_INCONG_VL_PR 1 0 2147483.647 + 59.089 1.692 ISI 0 0 0.000 + 60.781 2.047 V_RIGHT 0 0 0.000 + 62.828 2.114 ISI 0 0 0.000 + 64.942 2.026 CONG_RIGHT 0 0 0.000 + 66.968 2.180 ISI 0 0 0.000 + 69.148 1.693 RESPONSE_CONG_RIGHT 1 0 2147483.647 + 70.841 2.046 ISI 0 0 0.000 + 72.887 2.048 INCONG_VR_PL 0 0 0.000 + 74.935 2.180 ISI 0 0 0.000 + 77.115 2.027 P_RIGHT 0 0 0.000 + 79.142 2.180 ISI 0 0 0.000 + 81.322 2.047 CONG_LEFT 0 0 0.000 + 83.369 2.160 ISI 0 0 0.000 + 85.529 2.047 V_LEFT 0 0 0.000 + 87.576 1.980 ISI 0 0 0.000 + 89.556 1.692 RESPONSE_V_LEFT 1 0 2147483.647 + 91.248 1.914 ISI 0 0 0.000 + 93.162 2.026 REST 0 0 0.000 + 95.188 2.114 ISI 0 0 0.000 + 97.302 2.047 REST 0 0 0.000 + 99.349 1.759 ISI 0 0 0.000 + 101.108 2.046 CONG_RIGHT 0 0 0.000 + 103.154 1.692 ISI 0 0 0.000 + 104.846 2.047 REST 0 0 0.000 + 106.893 2.381 ISI 0 0 0.000 + 109.274 2.028 P_LEFT 0 0 0.000 + 111.302 1.913 ISI 0 0 0.000 + 113.215 1.692 RESPONSE_P_LEFT 1 0 2147483.647 + 114.907 2.180 ISI 0 0 0.000 + 117.087 2.047 REST 0 0 0.000 + 119.134 2.113 ISI 0 0 0.000 + 121.247 2.027 V_LEFT 0 0 0.000 + 123.274 2.381 ISI 0 0 0.000 + 125.655 2.048 INCONG_VL_PR 0 0 0.000 + 127.703 1.826 ISI 0 0 0.000 + 129.529 2.046 P_RIGHT 0 0 0.000 + 131.575 1.846 ISI 0 0 0.000 + 133.421 2.027 INCONG_VR_PL 0 0 0.000 + 135.448 1.913 ISI 0 0 0.000 + 137.361 1.692 RESPONSE_INCONG_VR_PL 1 0 2147483.647 + 139.053 2.247 ISI 0 0 0.000 + 141.300 2.047 V_RIGHT 0 0 0.000 + 143.347 2.382 ISI 0 0 0.000 + 145.729 2.026 CONG_LEFT 0 0 0.000 + 147.755 1.980 ISI 0 0 0.000 + 149.735 2.047 CONG_LEFT 0 0 0.000 + 151.782 1.893 ISI 0 0 0.000 + 153.675 1.692 RESPONSE_CONG_LEFT 1 0 2147483.647 + 155.367 2.113 ISI 0 0 0.000 + 157.480 2.047 REST 0 0 0.000 + 159.527 1.759 ISI 0 0 0.000 + 161.286 2.048 P_RIGHT 0 0 0.000 + 163.334 1.979 ISI 0 0 0.000 + 165.313 2.027 INCONG_VR_PL 0 0 0.000 + 167.340 1.980 ISI 0 0 0.000 + 169.320 2.046 REST 0 0 0.000 + 171.366 1.759 ISI 0 0 0.000 + 173.125 2.047 INCONG_VL_PR 0 0 0.000 + 175.172 1.960 ISI 0 0 0.000 + 177.132 2.046 P_LEFT 0 0 0.000 + 179.178 1.760 ISI 0 0 0.000 + 180.938 2.047 V_RIGHT 0 0 0.000 + 182.985 2.114 ISI 0 0 0.000 + 185.099 1.692 RESPONSE_V_RIGHT 1 0 2147483.647 + 186.791 2.247 ISI 0 0 0.000 + 189.038 2.047 V_LEFT 0 0 0.000 + 191.085 1.692 ISI 0 0 0.000 + 192.777 2.047 CONG_RIGHT 0 0 0.000 + 194.824 2.093 ISI 0 0 0.000 + 196.917 2.048 INCONG_VR_PL 0 0 0.000 + 198.965 2.247 ISI 0 0 0.000 + 201.212 1.692 RESPONSE_INCONG_VR_PL 1 0 2147483.647 + 202.904 1.779 ISI 0 0 0.000 + 204.683 2.027 CONG_LEFT 0 0 0.000 + 206.710 1.846 ISI 0 0 0.000 + 208.556 2.047 INCONG_VL_PR 0 0 0.000 + 210.603 1.959 ISI 0 0 0.000 + 212.562 2.048 REST 0 0 0.000 + 214.610 2.113 ISI 0 0 0.000 + 216.723 2.027 CONG_RIGHT 0 0 0.000 + 218.750 1.712 ISI 0 0 0.000 + 220.462 1.692 RESPONSE_CONG_RIGHT 1 0 2147483.647 + 222.154 2.047 ISI 0 0 0.000 + 224.201 2.027 REST 0 0 0.000 + 226.228 2.046 ISI 0 0 0.000 + 228.274 2.047 V_LEFT 0 0 0.000 + 230.321 2.161 ISI 0 0 0.000 + 232.482 2.047 V_RIGHT 0 0 0.000 + 234.529 1.846 ISI 0 0 0.000 + 236.375 2.026 P_LEFT 0 0 0.000 + 238.401 2.315 ISI 0 0 0.000 + 240.716 2.046 P_RIGHT 0 0 0.000 + 242.762 2.381 ISI 0 0 0.000 + 245.143 1.692 RESPONSE_P_RIGHT 1 0 2147483.647 + 246.835 2.047 ISI 0 0 0.000 + 248.882 2.048 V_LEFT 0 0 0.000 + 250.930 2.294 ISI 0 0 0.000 + 253.224 2.047 V_RIGHT 0 0 0.000 + 255.271 2.046 ISI 0 0 0.000 + 257.317 2.027 CONG_LEFT 0 0 0.000 + 259.344 1.913 ISI 0 0 0.000 + 261.257 2.046 P_LEFT 0 0 0.000 + 263.303 2.227 ISI 0 0 0.000 + 265.530 2.048 INCONG_VR_PL 0 0 0.000 + 267.578 1.846 ISI 0 0 0.000 + 269.424 2.027 P_RIGHT 0 0 0.000 + 271.451 2.381 ISI 0 0 0.000 + 273.832 1.712 RESPONSE_P_RIGHT 1 0 2147483.647 + 275.544 1.960 ISI 0 0 0.000 + 277.504 2.046 REST 0 0 0.000 + 279.550 2.114 ISI 0 0 0.000 + 281.664 2.026 REST 0 0 0.000 + 283.690 2.115 ISI 0 0 0.000 + 285.805 2.047 CONG_RIGHT 0 0 0.000 + 287.852 1.759 ISI 0 0 0.000 + 289.611 2.046 INCONG_VL_PR 0 0 0.000 + 291.657 1.693 ISI 0 0 0.000 + 293.350 1.712 RESPONSE_INCONG_VL_PR 1 0 2147483.647 + 295.062 2.294 ISI 0 0 0.000 + 297.356 2.047 V_LEFT 0 0 0.000 + 299.403 1.846 ISI 0 0 0.000 + 301.249 2.027 P_RIGHT 0 0 0.000 + 303.276 2.314 ISI 0 0 0.000 + 305.590 2.047 CONG_LEFT 0 0 0.000 + 307.637 2.381 ISI 0 0 0.000 + 310.018 1.692 RESPONSE_CONG_LEFT 1 0 2147483.647 + 311.710 2.314 ISI 0 0 0.000 + 314.024 2.047 CONG_RIGHT 0 0 0.000 + 316.071 2.180 ISI 0 0 0.000 + 318.251 2.028 REST 0 0 0.000 + 320.279 2.180 ISI 0 0 0.000 + 322.459 2.047 V_RIGHT 0 0 0.000 + 324.506 1.826 ISI 0 0 0.000 + 326.332 1.712 RESPONSE_V_RIGHT 1 0 2147483.647 + 328.044 1.893 ISI 0 0 0.000 + 329.937 2.047 INCONG_VR_PL 0 0 0.000 + 331.984 2.093 ISI 0 0 0.000 + 334.077 2.047 P_LEFT 0 0 0.000 + 336.124 2.248 ISI 0 0 0.000 + 338.372 2.047 INCONG_VL_PR 0 0 0.000 + 340.419 1.959 ISI 0 0 0.000 + 342.378 2.047 REST 0 0 0.000 + 344.425 2.381 ISI 0 0 0.000 + 346.806 1.360 BASE 0 0 0.000 diff --git a/demos/transformers/split_by_trials.m b/demos/transformers/split_by_trials.m new file mode 100644 index 00000000..bb474bf9 --- /dev/null +++ b/demos/transformers/split_by_trials.m @@ -0,0 +1,83 @@ +% Example of how to use transformers to: +% +% - "merge" certain trial type by renaming them using the Replace transformer +% - "split" the trials of certain conditions +% +% For MVPA analyses, this can be used to have 1 beta per trial (and not 1 per run per condition). +% +% (C) Copyright 2021 Remi Gau + +clear; + +tsv_file = fullfile(pwd, 'data', 'sub-03_task-VisuoTact_run-02_events.tsv'); + +data = bids.util.tsvread(tsv_file); + +data; + +% conditions_to_split = {'CONG_LEFT' +% 'CONG_RIGHT' +% 'INCONG_VL_PR' +% 'INCONG_VR_PL' +% 'P_LEFT' +% 'P_RIGHT' +% 'V_LEFT' +% 'V_RIGHT'}; + +% same but expressed as regular expressions +conditions_to_split = {'^.*LEFT$' + '^.*RIGHT$' + '^INCONG.*$'}; + +% columns headers where to store the new conditions +headers = {'LEFT' + 'RIGHT' + 'INCONG'}; + +%% merge responses + +transformers{1}.Name = 'Replace'; +transformers{1}.Input = 'trial_type'; +transformers{1}.Replace = struct('key', '^RESPONSE.*', 'value', 'RESPONSE'); +transformers{1}.Attribute = 'value'; + +%% split by trial + +for i = 1:numel(conditions_to_split) + + % create a new column where each event of a condition is labelled + % creates a "tmp" and "label" columns that are deleted after. + transformers{end + 1}.Name = 'Filter'; %#ok<*SAGROW> + transformers{end}.Input = 'trial_type'; + transformers{end}.Query = ['trial_type==' conditions_to_split{i}]; + transformers{end}.Output = 'tmp'; + + transformers{end + 1}.Name = 'LabelIdenticalRows'; + transformers{end}.Cumulative = true; + transformers{end}.Input = {'tmp'}; + transformers{end}.Output = {'label'}; + + transformers{end + 1}.Name = 'Concatenate'; + transformers{end}.Input = {'tmp', 'label'}; + transformers{end}.Output = headers(i); + + % clean up + % insert actual NaN + transformers{end + 1}.Name = 'Replace'; + transformers{end}.Input = headers(i); + transformers{end}.Replace = struct('key', '^NaN.*', 'value', 'n/a'); + transformers{end}.Attribute = 'value'; + + % remove temporary columns + transformers{end + 1}.Name = 'Delete'; + transformers{end}.Input = {'tmp', 'label'}; + +end + +[new_content, json] = bids.transformers(transformers, data); + +% save the new TSV for inspection sto make sure it looks like what we expect +bids.util.tsvwrite(fullfile(pwd, 'new_events.tsv'), new_content); + +% generate the transformation section that can be added to the bids stats model +bids.util.jsonencode(fullfile(pwd, 'transformers.json'), json); diff --git a/demos/working_with_files.m b/demos/working_with_files.m new file mode 100644 index 00000000..153074bc --- /dev/null +++ b/demos/working_with_files.m @@ -0,0 +1,170 @@ +% (C) Copyright 2021 Remi Gau + +% TODO turn into a notebook + +% demos to show how to use bids-matlab +% to create and edit file BIDS filenames +% +% bids.File is a class that helps you work with BIDS files +% +% - generate valid BIDS or BIDS-like filenames +% - parse existing filenames +% - edit those filenames +% - rename files +% - access that file metadata +% + +%% Parsing filenames +bf = bids.File('sub-01_ses-02_task-face_run-01_bold.nii.gz'); + +disp(bf.suffix); +disp(bf.entities); +disp(bf.extension); + +%% Changing parts of the filename + +bf = bids.File('sub-01_ses-02_task-face_run-01_bold.nii.gz'); +bf.entities.sub = '02'; + +disp(bf.filename); + +%% Removing part of the name + +bf = bids.File('sub-01_ses-02_task-face_run-01_bold.nii.gz'); +bf.entities.ses = ''; + +disp(bf.filename); + +%% Adding things to the name + +bf = bids.File('sub-01_task-face_run-01_bold.nii.gz'); +bf.entities.ses = '02'; + +% oops "ses-02" should be at the beginning +disp(bf.filename); + +% let's reorder things +bf = bf.reorder_entities(); + +% fixed +disp(bf.filename); + +% note this also works with the derivatives entities of BIDS +bf.entities.desc = 'mean'; +bf.entities.space = 'individual'; +bf.entities.hemi = 'L'; + +bf = bf.reorder_entities(); + +disp(bf.filename); + +%% Generating a filename from scratch + +% define the specification of the name to create +spec = struct('ext', '.eeg', ... + 'suffix', 'eeg', ... + 'entities', struct('task', 'lineCrossing', ... + 'run', '02', ... + 'sub', '01')); + +% by default the entities are ordered +% in the way you'entered them +bf = bids.File(spec); +disp(bf.filename); + +% but you can use the BIDS schema +% to make sure things are ordered the right way +bf = bids.File(spec, 'use_schema', true); +disp(bf.filename); + +%% Checking for valid BIDS filename + +% if we forget the required entity "task" for this eeg file +spec = struct('ext', '.eeg', ... + 'suffix', 'eeg', ... + 'entities', struct('run', '02', ... + 'sub', '01')); + +% using the verbose flag will throw a warning +bids.File(spec, 'use_schema', true, 'verbose', true); + +% using the tolerant flag will throw an error +bids.File(spec, 'use_schema', true, 'tolerant', false); + +%% Renaming existing files + +% let's create a dummy file to work with +% by calling the linux "touch" command +system('touch sub-01_ses-02_task-face_run-01_bold.nii.gz'); + +input_file = fullfile(pwd, 'sub-01_ses-02_task-face_run-01_bold.nii.gz'); + +% let's change the filename +bf = bids.File(input_file); +bf.entities.sub = '02'; +bf.entities.ses = ''; +bf.entities.run = ''; + +% use the rename method to see how the file WOULD be renamed +bf.rename('verbose', true); + +% use "dry_run" false to actually rename the file +bf.rename('verbose', true, ... + 'dry_run', false); + +% you may need to use the 'force' parameter +% if you want to overwrite existing files +% +% bf.rename('verbose', true, ... +% 'dry_run', false, ... +% 'force', true); + +% let's add a test to make sure the expected file exist +expected_file = fullfile(pwd, 'sub-02_task-face_bold.nii.gz'); +assert(exist(expected_file, 'file') == 2); + +% we clean up the mess we did +delete(expected_file); + +%% Renaming existing files with specification + +% same as above but allows you to specify all the changes to apply +% in a single "spec" structure + +system('touch sub-01_ses-02_task-face_run-01_bold.nii.gz'); +input_file = fullfile(pwd, 'sub-01_ses-02_task-face_run-01_bold.nii.gz'); + +bf = bids.File(input_file); + +spec = struct('entities', struct('desc', 'mean', ... + 'ses', '', ... + 'run', '')); + +bf.rename('spec', spec, ... + 'verbose', true, ... + 'dry_run', false); + +expected_file = fullfile(pwd, 'sub-01_task-face_desc-mean_bold.nii.gz'); +assert(exist(expected_file, 'file') == 2); + +delete('*.nii.gz'); + +%% Accessing metadata + +% creating dummy data +system('touch sub-01_ses-02_task-face_run-01_bold.nii.gz'); +% creating dummy metadata +bids.util.jsonencode('sub-01_ses-02_task-face_run-01_bold.json', ... + struct('TaskName', 'face', ... + 'RepetitionTime', 1.5)); +% access metadata +input_file = fullfile(pwd, 'sub-01_ses-02_task-face_run-01_bold.nii.gz'); + +bf = bids.File(input_file); + +disp(bf.metadata.TaskName); + +disp(bf.metadata.RepetitionTime); + +delete('*.nii.gz'); +delete('*.json'); diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 9f9db39e..00000000 --- a/docs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# documentation -build/* diff --git a/docs/Makefile b/docs/Makefile index d0c3cbf1..a359dce0 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,6 +1,17 @@ # Minimal makefile for Sphinx documentation # +define BROWSER_PYSCRIPT +import os, webbrowser, sys + +from urllib.request import pathname2url + +webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) +endef +export BROWSER_PYSCRIPT + +BROWSER := python -c "$$BROWSER_PYSCRIPT" + # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= @@ -18,3 +29,7 @@ help: # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + + +view: + $(BROWSER) build/html/index.html diff --git a/docs/bids-matlab.pdf b/docs/bids-matlab.pdf new file mode 100644 index 00000000..bdb720af Binary files /dev/null and b/docs/bids-matlab.pdf differ diff --git a/commenting_images/BIDS_GitHub_mainpage.png b/docs/commenting_images/BIDS_GitHub_mainpage.png similarity index 100% rename from commenting_images/BIDS_GitHub_mainpage.png rename to docs/commenting_images/BIDS_GitHub_mainpage.png diff --git a/commenting_images/BIDS_comment.png b/docs/commenting_images/BIDS_comment.png similarity index 100% rename from commenting_images/BIDS_comment.png rename to docs/commenting_images/BIDS_comment.png diff --git a/commenting_images/BIDS_comment_block.png b/docs/commenting_images/BIDS_comment_block.png similarity index 100% rename from commenting_images/BIDS_comment_block.png rename to docs/commenting_images/BIDS_comment_block.png diff --git a/commenting_images/BIDS_file_comment.png b/docs/commenting_images/BIDS_file_comment.png similarity index 100% rename from commenting_images/BIDS_file_comment.png rename to docs/commenting_images/BIDS_file_comment.png diff --git a/commenting_images/BIDS_pr.png b/docs/commenting_images/BIDS_pr.png similarity index 100% rename from commenting_images/BIDS_pr.png rename to docs/commenting_images/BIDS_pr.png diff --git a/commenting_images/BIDS_pr_accept_comment.png b/docs/commenting_images/BIDS_pr_accept_comment.png similarity index 100% rename from commenting_images/BIDS_pr_accept_comment.png rename to docs/commenting_images/BIDS_pr_accept_comment.png diff --git a/commenting_images/BIDS_pr_commit_batch.png b/docs/commenting_images/BIDS_pr_commit_batch.png similarity index 100% rename from commenting_images/BIDS_pr_commit_batch.png rename to docs/commenting_images/BIDS_pr_commit_batch.png diff --git a/commenting_images/BIDS_pr_files_changed.png b/docs/commenting_images/BIDS_pr_files_changed.png similarity index 100% rename from commenting_images/BIDS_pr_files_changed.png rename to docs/commenting_images/BIDS_pr_files_changed.png diff --git a/commenting_images/BIDS_pr_list.png b/docs/commenting_images/BIDS_pr_list.png similarity index 100% rename from commenting_images/BIDS_pr_list.png rename to docs/commenting_images/BIDS_pr_list.png diff --git a/commenting_images/BIDS_pr_reviewer_credit.png b/docs/commenting_images/BIDS_pr_reviewer_credit.png similarity index 100% rename from commenting_images/BIDS_pr_reviewer_credit.png rename to docs/commenting_images/BIDS_pr_reviewer_credit.png diff --git a/commenting_images/BIDS_suggest.png b/docs/commenting_images/BIDS_suggest.png similarity index 100% rename from commenting_images/BIDS_suggest.png rename to docs/commenting_images/BIDS_suggest.png diff --git a/commenting_images/BIDS_suggest_change.png b/docs/commenting_images/BIDS_suggest_change.png similarity index 100% rename from commenting_images/BIDS_suggest_change.png rename to docs/commenting_images/BIDS_suggest_change.png diff --git a/commenting_images/BIDS_suggest_text.png b/docs/commenting_images/BIDS_suggest_text.png similarity index 100% rename from commenting_images/BIDS_suggest_text.png rename to docs/commenting_images/BIDS_suggest_text.png diff --git a/commenting_images/gitflow_diagram.png b/docs/commenting_images/gitflow_diagram.png similarity index 100% rename from commenting_images/gitflow_diagram.png rename to docs/commenting_images/gitflow_diagram.png diff --git a/docs/generate_doc.py b/docs/generate_doc.py new file mode 100644 index 00000000..42ffae69 --- /dev/null +++ b/docs/generate_doc.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from pathlib import Path + +from rich import print + +code_src = Path(__file__).parent.parent.joinpath("+bids") + +doc_src = Path(__file__).parent.joinpath("source") + +bidspm_file = doc_src.joinpath("dev_doc.rst") + +dir_ignore_list = ("+util", "+transformers_list") + +file_ignore_list = "" + + +def return_title(path: Path, parent_folder=None): + + tmp = f"{path.name}" if parent_folder is None else f"{parent_folder} {path.name}" + tmp.replace("_", " ") + + title = f"\n\n.. _{tmp}:\n" + title += f"\n{tmp}\n" + title += "=" * len(tmp) + "\n" + + return title + + +def append_dir_content(path: Path, content: str, parent_folder=None, recursive=False): + + if not path.is_dir(): + return content + + m_files = sorted(list(path.glob("*.m"))) + + if len(m_files) > 0: + title = return_title(path=path, parent_folder=parent_folder) + content += title + + for file in m_files: + + if file.stem in file_ignore_list: + continue + + content += f".. _{file.stem}:\n" + if parent_folder is None: + function_name = f"+bids.{path.name}.{file.stem}" + else: + function_name = f"src.{parent_folder}.{path.name}.{file.stem}" + content += f".. autofunction:: {function_name}\n" + + print(function_name) + + if recursive and path.is_dir(): + print(path) + for subpath in path.iterdir(): + content = append_dir_content( + subpath, content, parent_folder=path.name, recursive=recursive + ) + + return content + + +def main(): + + with bidspm_file.open("w", encoding="utf8") as f: + + content = """.. AUTOMATICALLY GENERATED + +.. _dev_doc: + +developer documentation +*********************** +""" + + subfolders = sorted(list(code_src.iterdir())) + + for path in subfolders: + + if path.name in dir_ignore_list: + continue + + if path.is_dir(): + content = append_dir_content( + path, content, parent_folder=None, recursive=True + ) + + print(content, file=f) + + with bidspm_file.open("r", encoding="utf8") as f: + content = f.read() + + # print(content) + + +if __name__ == "__main__": + main() diff --git a/docs/requirements.txt b/docs/requirements.txt index aa3e0882..54a2fca5 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,3 +3,5 @@ sphinxcontrib-matlabdomain sphinxcontrib-napoleon sphinx_rtd_theme sphinx-copybutton +myst-parser +rich diff --git a/docs/source/conf.py b/docs/source/conf.py index b0836f08..c390c2b3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,17 +12,19 @@ # import os import sys -sys.path.insert(0, os.path.abspath('../..')) + +sys.path.insert(0, os.path.abspath("../..")) # -- Project information ----------------------------------------------------- -project = 'bids-matlab' -copyright = '2018, BIDS-MATLAB developers' -author = 'BIDS-MATLAB developers' +project = "bids-matlab" +copyright = "2018, BIDS-MATLAB developers" +author = "BIDS-MATLAB developers" # The full version, including alpha/beta/rc tags -release = 'v0.1.0' +with open("../../version.txt", encoding="utf-8") as version_file: + release = version_file.read() # -- General configuration --------------------------------------------------- @@ -31,14 +33,16 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinxcontrib.matlab', - 'sphinx.ext.autodoc', - 'sphinx_copybutton'] -matlab_src_dir = os.path.dirname(os.path.abspath('../../+bids')) -primary_domain = 'mat' + "sphinxcontrib.matlab", + "sphinx.ext.autodoc", + "sphinx_copybutton", + "myst_parser", +] +matlab_src_dir = os.path.dirname(os.path.abspath("../../+bids")) +primary_domain = "mat" # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -46,15 +50,15 @@ exclude_patterns = [] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # The master toctree document. -master_doc = 'index' +master_doc = "index" # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" -autodoc_member_order = 'bysource' +autodoc_member_order = "bysource" # -- Options for HTML output ------------------------------------------------- @@ -62,7 +66,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -84,11 +88,11 @@ # } html_sidebars = { - '**': [ - 'about.html', - 'navigation.html', - 'relations.html', # needs 'show_related': True theme option to display - 'searchbox.html', - 'donate.html', + "**": [ + "about.html", + "navigation.html", + "relations.html", # needs 'show_related': True theme option to display + "searchbox.html", + "donate.html", ] } diff --git a/docs/source/dev_doc.rst b/docs/source/dev_doc.rst new file mode 100644 index 00000000..6d8edb8d --- /dev/null +++ b/docs/source/dev_doc.rst @@ -0,0 +1,68 @@ +.. AUTOMATICALLY GENERATED + +.. _dev_doc: + +developer documentation +*********************** + + +.. _+internal: + ++internal +========= +.. _add_missing_field: +.. autofunction:: +bids.+internal.add_missing_field +.. _append_to_layout: +.. autofunction:: +bids.+internal.append_to_layout +.. _camel_case: +.. autofunction:: +bids.+internal.camel_case +.. _create_unordered_list: +.. autofunction:: +bids.+internal.create_unordered_list +.. _download: +.. autofunction:: +bids.+internal.download +.. _ends_with: +.. autofunction:: +bids.+internal.ends_with +.. _error_handling: +.. autofunction:: +bids.+internal.error_handling +.. _file_utils: +.. autofunction:: +bids.+internal.file_utils +.. _format_path: +.. autofunction:: +bids.+internal.format_path +.. _get_meta_list: +.. autofunction:: +bids.+internal.get_meta_list +.. _get_metadata: +.. autofunction:: +bids.+internal.get_metadata +.. _get_version: +.. autofunction:: +bids.+internal.get_version +.. _is_github_ci: +.. autofunction:: +bids.+internal.is_github_ci +.. _is_octave: +.. autofunction:: +bids.+internal.is_octave +.. _is_valid_fieldname: +.. autofunction:: +bids.+internal.is_valid_fieldname +.. _keep_file_for_query: +.. autofunction:: +bids.+internal.keep_file_for_query +.. _list_all_trial_types: +.. autofunction:: +bids.+internal.list_all_trial_types +.. _match_structure_fields: +.. autofunction:: +bids.+internal.match_structure_fields +.. _parse_filename: +.. autofunction:: +bids.+internal.parse_filename +.. _plot_diagnostic_table: +.. autofunction:: +bids.+internal.plot_diagnostic_table +.. _regexify: +.. autofunction:: +bids.+internal.regexify +.. _replace_placeholders: +.. autofunction:: +bids.+internal.replace_placeholders +.. _return_file_index: +.. autofunction:: +bids.+internal.return_file_index +.. _return_file_info: +.. autofunction:: +bids.+internal.return_file_info +.. _return_subject_index: +.. autofunction:: +bids.+internal.return_subject_index +.. _root_dir: +.. autofunction:: +bids.+internal.root_dir +.. _starts_with: +.. autofunction:: +bids.+internal.starts_with +.. _url: +.. autofunction:: +bids.+internal.url diff --git a/docs/source/function_description.rst b/docs/source/function_description.rst index 05afb602..a41d06cc 100644 --- a/docs/source/function_description.rst +++ b/docs/source/function_description.rst @@ -14,3 +14,23 @@ Function description .. autofunction:: report .. autofunction:: validate + +.. autofunction:: diagnostic + +.. _fig_diagnostic: +.. figure:: images/MultisubjectMultimodalFaceProcessing.png + :align: center + + output of ``diagnostic`` + +.. _fig_diagnostic_events: +.. figure:: images/Simon-task_func_Simontask.png + :align: center + + output of ``diagnostic`` for events + +.. _fig_diagnostic_task: +.. figure:: images/MultisubjectMultimodalFaceProcessing_splitby-task.png + :align: center + + output of ``diagnostic`` split by task diff --git a/docs/source/general_information.md b/docs/source/general_information.md new file mode 100644 index 00000000..3ed5fe76 --- /dev/null +++ b/docs/source/general_information.md @@ -0,0 +1,216 @@ + + + + +[![miss_hit](https://github.com/bids-standard/bids-matlab/actions/workflows/miss_hit.yml/badge.svg)](https://github.com/bids-standard/bids-matlab/actions/workflows/miss_hit.yml) +[![tests_matlab](https://github.com/bids-standard/bids-matlab/actions/workflows/run_tests_matlab.yml/badge.svg)](https://github.com/bids-standard/bids-matlab/actions/workflows/run_tests_matlab.yml) +[![tests_octave](https://github.com/bids-standard/bids-matlab/actions/workflows/run_tests_octave.yml/badge.svg)](https://github.com/bids-standard/bids-matlab/actions/workflows/run_tests_octave.yml) +[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/bids-standard/bids-matlab/master?filepath=examples/tutorial.ipynb) +[![View bids-matlab on File Exchange](https://www.mathworks.com/matlabcentral/images/matlab-file-exchange.svg)](https://nl.mathworks.com/matlabcentral/fileexchange/93740-bids-matlab) +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.5910584.svg)](https://doi.org/10.5281/zenodo.5910584) +[![All Contributors](https://img.shields.io/badge/all_contributors-13-orange.svg?style=flat-square)](#contributors-) + + + +# BIDS for MATLAB / Octave + +This repository aims at centralising MATLAB/Octave tools to interact with +datasets conforming to the BIDS (Brain Imaging Data Structure) format. + +For more information about BIDS, visit https://bids.neuroimaging.io/. + +Join our chat on the +[BIDS-MATLAB channel](https://mattermost.brainhack.org/brainhack/channels/bids-matlab) +on the brainhack mattermost and our [google group](https://groups.google.com/g/bids-matlab). + +See also [PyBIDS](https://github.com/bids-standard/pybids) for Python and the +[BIDS Starter Kit](https://github.com/bids-standard/bids-starter-kit). + +## Installation + +Download, unzip this repository and add its content to the MATLAB/Octave path. + +```Matlab +unzip('https://github.com/bids-standard/bids-matlab/archive/master.zip'); +addpath('bids-matlab-master'); +``` + +Or clone it with git: + +```bash +git clone https://github.com/bids-standard/bids-matlab.git +``` + +and then add it to your MATLAB/Octave path. + +```Matlab +addpath('bids-matlab'); +``` + +### Get the latest features + +The latest features of bids-matlab that are in development are in our `dev` +"branch". + +To access them you can either download the `dev` branch from there: +https://github.com/bids-standard/bids-matlab/tree/dev + +Or you can check it out the `dev` branch after the adding this official +bids-matlab repository as a remote. + +``` +git add remote upstream https://github.com/bids-standard/bids-matlab.git +git checkout upstream/dev +``` + +## Features + +### What this toolbox can do + +- read the layout of a BIDS dataset (see `bids.layout`), +- perform queries on that layout to get information about the subjects, + sessions, runs, modalities, metadata... contained within that dataset (see + `bids.query`), +- parse the layout of "BIDS-derivative compatible" datasets (like those + generated by fMRIprep), + +- create BIDS compatible filenames or folder structures for raw or derivatives + datasets (`bids.File`, `bids.util.mkdir`, + `bids.dataset_description`), +- do basic copying of files to help initialize a derivative dataset + to start a new analysis (`bids.copy_to_derivative`), + +- generate a human readable report of the content of BIDS data set containing + anatomical MRI, functional MRI, diffusion weighted imaging, field map data + (see `bids.report`) +- read and write JSON files (see `bids.util.jsondecode` and + `bids.util.jsonwrite`) provided that the right + [dependencies](#reading-and-writing-json-files) are installed, +- read and write TSV files (see `bids.util.tsvread` and `bids.util.tsvwrite`) + +- access and query the BIDS schema (bids.schema) + +The behavior of this toolbox assumes that it is interacting with a valid BIDS +dataset that should have been validated using +[BIDS-validator](https://bids-standard.github.io/bids-validator/). If the +Node.js version of the validator is +[installed on your computer](https://github.com/bids-standard/bids-validator#quickstart), +you can call it from the matlab prompt using `bids.validate`. Just be aware that +any unvalidated components may produce undefined behavior. Although, if you're +BIDS-y enough, the behavior may be predictable. + +### What this toolbox cannot do... yet + +- generate human readable reports of the content of BIDS data with EEG, MEG, + iEEG, physio and events data, +- deal with some of the incoming BIDS extensions (BIDS model...) + +### What will this toolbox most likely never do + +- act as a Matlab / Octave based BIDS-validator +- act as a BIDS converter +- implement reading / writing capabilities for the different modality-specific + data format that exist in BIDS (`.nii`, `.eeg`, `.ds`, `.fif`...) + +## Usage + +BIDS matlab is structured as package, so you can easily access functions in subfolders +that start with `+`. + +To use the `+bids/layout.m` function: + +```Matlab +BIDS = bids.layout('/home/data/ds000117'); +bids.query(BIDS, 'subjects') +``` + +To use the `+bids/+util/jsondecode.m` function: + +```Matlab +content = bids.util.jsondecode('/home/data/some_json_file.json'); +``` + +A +[tutorial](https://github.com/bids-standard/bids-matlab/blob/master/examples/tutorial.ipynb) +is available as a Jupyter Notebook and can be run interactively via +[Binder](https://mybinder.org/v2/gh/bids-standard/bids-matlab/master?filepath=examples/tutorial.ipynb). + +## Requirements + +BIDS-MATLAB works with: + +- Octave 5.2.0 or newer +- MATLAB R2014a or newer + +We aim for compatibility with the latest stable release of Octave at any time. +Compatibility can sometimes also be achieved with older versions of Octave but +this is not guaranteed. + +### Reading and writing JSON files + +If you are using MATLAB R2016b or newer, nothing else needs to be installed. + +If you are using MATLAB R2016a or older, or using Octave, you need to install a +supported JSON library for your MATLAB or Octave. This can be any of: + +- [JSONio](https://github.com/gllmflndn/JSONio) for MATLAB or Octave +- [SPM12](https://www.fil.ion.ucl.ac.uk/spm/software/spm12/) + +## Implementation + +Starting point was `spm_BIDS.m` from [SPM12](https://github.com/spm/spm12) +([documentation](https://en.wikibooks.org/wiki/SPM/BIDS#BIDS_parser_and_queries)) +reformatted in a `+bids` package with dependencies to other SPM functions +removed. + +## Other tools (MATLAB only) + +- [dicm2nii](https://github.com/xiangruili/dicm2nii): A DICOM to BIDS + converter +- [imtool3D_BIDS](https://github.com/tanguyduval/imtool3D_td): A 3D viewer for + BIDS directory +- [Brainstorm](https://github.com/brainstorm-tools/brainstorm3): Comprehensive + brain analysis toolbox (includes BIDS + [import and export](https://neuroimage.usc.edu/brainstorm/ExportBids) and + different examples dealing with BIDS datasets (e.g. + [group analysis from a MEG visual dataset](https://neuroimage.usc.edu/brainstorm/Tutorials/VisualGroup), + [resting state analysis from OMEGA datasets](https://neuroimage.usc.edu/brainstorm/Tutorials/RestingOmega#BIDS_specifications) + ) + +## Contributors ✨ + +Thanks goes to these wonderful people +([emoji key](https://allcontributors.org/docs/en/emoji-key)): + + + + + + + + + + + + + + + + + + + + + + + +

Guillaume

💻 🎨 📖 💡 🤔 🚇 🚧 💬 👀 ⚠️

Remi Gau

💻 🎨 📖 💡 🤔 🚧 💬 👀 ⚠️

Andrew Janke

💻 🎨 📖 🤔 👀 🚇

tanguyduval

💻 📖 🤔

Robert Oostenveld

💻 📖 🤔 👀

Christopher Madan

🖋

Julia Guiomar Niso Galán

👀

Michał Szczepanik

🚇 🤔 💻

Henk Mutsaerts

💻 🤔

Nikita Beliy

💻 🤔 👀

Martin Norgaard

🐛 🤔

Cyril Pernet

💻 🤔

Christophe Phillips

🤔

CerenB

👀
+ + + + + + +This project follows the +[all-contributors](https://github.com/all-contributors/all-contributors) +specification. Contributions of any kind welcome! diff --git a/docs/source/images/MultisubjectMultimodalFaceProcessing.png b/docs/source/images/MultisubjectMultimodalFaceProcessing.png new file mode 100644 index 00000000..c7f96ed3 Binary files /dev/null and b/docs/source/images/MultisubjectMultimodalFaceProcessing.png differ diff --git a/docs/source/images/MultisubjectMultimodalFaceProcessing_splitby-task.png b/docs/source/images/MultisubjectMultimodalFaceProcessing_splitby-task.png new file mode 100644 index 00000000..b412341f Binary files /dev/null and b/docs/source/images/MultisubjectMultimodalFaceProcessing_splitby-task.png differ diff --git a/docs/source/images/Simon-task_func_Simontask.png b/docs/source/images/Simon-task_func_Simontask.png new file mode 100644 index 00000000..169055ed Binary files /dev/null and b/docs/source/images/Simon-task_func_Simontask.png differ diff --git a/docs/source/images/plot_events_ds001.png b/docs/source/images/plot_events_ds001.png new file mode 100644 index 00000000..6878a447 Binary files /dev/null and b/docs/source/images/plot_events_ds001.png differ diff --git a/docs/source/index.rst b/docs/source/index.rst index 333ece1d..659d1393 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -16,11 +16,15 @@ To see how to install BIDS-Matlab, please check :maxdepth: 3 :caption: Content + general_information layout_query file function_description utility_functions + stats_model + transformers schema + dev_doc Indices and tables diff --git a/docs/source/stats_model.rst b/docs/source/stats_model.rst new file mode 100644 index 00000000..cb699be7 --- /dev/null +++ b/docs/source/stats_model.rst @@ -0,0 +1,11 @@ +BIDS stats model handling +************************* + +See the `BIDS stats model website +`_ +for more information. + +.. automodule:: +bids + +.. autoclass:: Model + :members: diff --git a/docs/source/transformers.rst b/docs/source/transformers.rst new file mode 100644 index 00000000..5ca8f165 --- /dev/null +++ b/docs/source/transformers.rst @@ -0,0 +1,156 @@ +Transformers +************ + +Those transformers are meant to be used to manipulate the content of TSV files once +loaded as structure with ``bids.util.tsvread``. + +They are mostly meant to be used to implement the transformations described in BIDS +stats models but can also be used to manipulate TSV files in batches. + +For each type of transformer, we describe first how they are meant to be "called" +in the JSON file of the BIDS stats model. + +There is also an code example to show how to use them. + +The behavior and their "call" in JSON should (hopefully) be fairly close to the +`pybids-transformers `_. + +Applying transformations +======================== + +An "array" of transformations can be applied one after the other using +``bids.transformers()``. + +.. automodule:: +bids + +.. autofunction:: transformers + +.. automodule:: +bids.+transformers_list + +Basic operations +================ + +- Add +- Subtract +- Multiply +- Divide +- Power + +.. autofunction:: Basic + +Logical operations +================== + +- And +- Or +- Not + +.. autofunction:: Logical + + +Munge operations +================ + +Transformations that primarily involve manipulating/munging variables into +other formats or shapes. + +Assign +------ + +.. autofunction:: Assign + +Concatenate +----------- + +.. autofunction:: Concatenate + +Copy +---- + +.. autofunction:: Copy + +Delete +------ + +.. autofunction:: Delete + +DropNA +------ + +.. autofunction:: Drop_na + +Factor +------ + +.. autofunction:: Factor + +Filter +------- + +.. autofunction:: Filter + +Label identical rows +-------------------- + +.. autofunction:: Label_identical_rows + +Merge identical rows +-------------------- + +.. autofunction:: Merge_identical_rows + +Replace +------- + +.. autofunction:: Replace + +Select +------ + +.. autofunction:: Select + +Split +----- + +.. autofunction:: Split + + +Compute operations +================== + +Transformations that primarily involve numerical computation on variables. + +Constant +-------- + +.. autofunction:: Constant + +Mean +----- + +.. autofunction:: Mean + +Product +------- + +.. autofunction:: Product + +Scale +----- + +.. autofunction:: Scale + +Std +--- + +.. autofunction:: Std + +Sum +--- + +.. autofunction:: Sum + +Threshold +--------- + +.. autofunction:: Threshold diff --git a/docs/source/utility_functions.rst b/docs/source/utility_functions.rst index 24a23d51..a5087bce 100644 --- a/docs/source/utility_functions.rst +++ b/docs/source/utility_functions.rst @@ -3,10 +3,22 @@ Utility functions .. automodule:: +bids.+util +.. autofunction:: create_data_dict +.. autofunction:: create_participants_tsv +.. autofunction:: create_readme +.. autofunction:: create_scans_tsv +.. autofunction:: create_sessions_tsv +.. autofunction:: download_ds .. autofunction:: jsondecode -.. autofunction:: jsonwrite +.. autofunction:: jsonencode +.. autofunction:: mkdir +.. autofunction:: plot_events + +.. _fig_diagsnotic: +.. figure:: images/plot_events_ds001.png + :align: center + + output of ``plot_events`` .. autofunction:: tsvread .. autofunction:: tsvwrite - -.. autofunction:: mkdir diff --git a/examples/.gitignore b/examples/.gitignore deleted file mode 100644 index 6216d193..00000000 --- a/examples/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ - -bids-examples -dummy_ds -output -.ipynb_checkpoints diff --git a/examples/01_BIDS-Matlab_basics.ipynb b/examples/01_BIDS-Matlab_basics.ipynb deleted file mode 100644 index 825806ef..00000000 --- a/examples/01_BIDS-Matlab_basics.ipynb +++ /dev/null @@ -1,878 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "tags": [] - }, - "source": [ - "# BIDS-Matlab: basics\n", - "\n", - "1. [Indexing a dataset](#Indexing-a-dataset)\n", - "1. [Querying a dataset](#Querying-a-dataset)\n", - " 1. [Get filenames](#Get-filenames)\n", - " 1. [Get metadata](#Get-metadata)\n", - " 1. [Get \"dependencies\" of a given file](#Get-\"dependencies\"-of-a-given-file) \n", - " " - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [], - "source": [ - "% add bids-matlab to path\n", - "addpath(fullfile(pwd, '..'));" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We will work with the \"empty\" dataset from the bids-examples repository : https://github.com/bids-standard/bids-examples" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We use a bit of command line magic to view the top (`head`) directories (`-d`) at a certain level depth (`-L 2`)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's work on the `ds101` dataset." - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "bids-examples/ds101\n", - "├── sub-01\n", - "│   ├── anat\n", - "│   └── func\n", - "├── sub-02\n", - "│   ├── anat\n", - "│   └── func\n", - "├── sub-03\n", - "│   ├── anat\n", - "│   └── func\n", - "\n" - ] - } - ], - "source": [ - "!tree bids-examples/ds101 -d -L 2 | head" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Indexing a dataset\n", - "\n", - "This is done with the `bids.layout` function." - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - " Parse a directory structure formatted according to the BIDS standard\n", - "\n", - " USAGE::\n", - "\n", - " BIDS = bids.layout(pwd, ...\n", - " 'use_schema', true, ...\n", - " 'index_derivatives', false, ...\n", - " 'tolerant', true, ...\n", - " 'verbose', false)\n", - "\n", - " :param root: directory of the dataset formatted according to BIDS\n", - " [default: ``pwd``]\n", - " :type root: string\n", - "\n", - " :param use_schema: If set to ``true``, the parsing of the dataset\n", - " will follow the bids-schema provided with bids-matlab.\n", - " If set to ``false`` files just have to be of the form\n", - " ``sub-label_[entity-label]_suffix.ext`` to be parsed.\n", - " If a folder path is provided, then the schema contained\n", - " in that folder will be used for parsing.\n", - " :type use_schema: boolean\n", - "\n", - " :param index_derivatives: if ``true`` this will index the content of the\n", - " any ``derivatives`` folder in the BIDS dataset.\n", - " :type index_derivatives: boolean\n", - "\n", - " :param tolerant: Set to ``true`` to turn validation errors into warnings\n", - " :type tolerant: boolean\n", - "\n", - " :param verbose: Set to ``true`` to get more feedback\n", - " :type verbose: boolean\n", - "\n", - "\n", - " (C) Copyright 2016-2018 Guillaume Flandin, Wellcome Centre for Human Neuroimaging\n", - "\n", - " (C) Copyright 2018 BIDS-MATLAB developers\n", - "\n", - "\n", - "Additional help for built-in functions and operators is\n", - "available in the online version of the manual. Use the command\n", - "'doc ' to search the manual index.\n", - "\n", - "Help and information about Octave is also available on the WWW\n", - "at http://www.octave.org and via the help@octave.org\n", - "mailing list.\n" - ] - } - ], - "source": [ - "help bids.layout" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "BIDS = bids.layout(fullfile(pwd,'bids-examples','ds101'));" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Querying a dataset\n", - "\n", - "Make general queries about the dataset are with `bids.query` made on the layout returned by `bids.layout`." - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - " Queries a directory structure formatted according to the BIDS standard\n", - "\n", - " USAGE::\n", - "\n", - " result = bids.query(BIDS, query, ...)\n", - "\n", - " :param BIDS: BIDS directory name or BIDS structure (from bids.layout)\n", - " :type BIDS: structure or string\n", - " :param query: type of query (see list below)\n", - " :type query: string\n", - "\n", - " Type of query allowed:\n", - "\n", - " - ``'sessions'``\n", - " - ``'subjects'``\n", - " - ``'modalities'``\n", - " - ``'tasks'``\n", - " - ``'runs'``\n", - " - ``'suffixes'``\n", - " - ``'entities'``\n", - " - ``'data'``\n", - " - ``'metadata'``\n", - " - ``'metafiles'``\n", - " - ``'dependencies'``\n", - " - ``'extensions'``\n", - " - ``'prefixes'``\n", - "\n", - "\n", - " .. warning:: Note that all the query types are plurals.\n", - "\n", - " Queries can \"filtered\" by passing more arguments key-value pairs as a list of\n", - " strings or as a cell or a structure.\n", - "\n", - " Note that for the entities listed below can be queried using integers:\n", - "\n", - " - ``'run'``\n", - " - ``'flip'``\n", - " - ``'inv'``\n", - " - ``'split'``\n", - " - ``'echo'``\n", - "\n", - " It is also possible to use regular expressions in the value.\n", - "\n", - " Regex example::\n", - "\n", - " % The following 2 will return the same thing\n", - " data = bids.query(BIDS, 'data', 'sub', '01')\n", - " data = bids.query(BIDS, 'data', 'sub', '^01$')\n", - "\n", - " % But the following would return all the data for all subjects\n", - " % whose label ends in '01'\n", - " data = bids.query(BIDS, 'data', 'sub', '.*01')\n", - "\n", - " ---\n", - "\n", - " Example 1::\n", - "\n", - " data = bids.query(BIDS, 'data', ...\n", - " 'sub', '01', ...\n", - " 'task', 'stopsignalwithpseudowordnaming', ...\n", - " 'run', 1:5, ...\n", - " 'extension', '.nii.gz', ...\n", - " 'suffix', 'bold');\n", - "\n", - "\n", - " Example 2::\n", - "\n", - " filters = struct('sub', '01', ...\n", - " 'task', 'stopsignalwithpseudowordnaming', ...\n", - " 'run', 1:5, ...\n", - " 'extension', '.nii.gz', ...\n", - " 'suffix', 'bold');\n", - "\n", - " data = bids.query(BIDS, 'data', filters);\n", - "\n", - "\n", - " Example 3::\n", - "\n", - " filters = {'sub', '0[1-5]'; ...\n", - " 'task', 'stopsignal.*'; ...\n", - " 'run', 1:5; ...\n", - " 'extension', '.nii.*'; ...\n", - " 'suffix', 'bold'};\n", - "\n", - " data = bids.query(BIDS, 'data', filters);\n", - "\n", - "\n", - " (C) Copyright 2016-2018 Guillaume Flandin, Wellcome Centre for Human Neuroimaging\n", - "\n", - " (C) Copyright 2018 BIDS-MATLAB developers\n", - "\n", - "\n", - "Additional help for built-in functions and operators is\n", - "available in the online version of the manual. Use the command\n", - "'doc ' to search the manual index.\n", - "\n", - "Help and information about Octave is also available on the WWW\n", - "at http://www.octave.org and via the help@octave.org\n", - "mailing list.\n" - ] - } - ], - "source": [ - "help bids.query" - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = \n", - "{\n", - " [1,1] = run\n", - " [2,1] = sub\n", - " [3,1] = task\n", - "}\n" - ] - } - ], - "source": [ - "bids.query(BIDS, 'entities')" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = \n", - "{\n", - " [1,1] = 01\n", - " [1,2] = 02\n", - " [1,3] = 03\n", - " [1,4] = 04\n", - " [1,5] = 05\n", - " [1,6] = 06\n", - " [1,7] = 07\n", - " [1,8] = 08\n", - " [1,9] = 09\n", - " [1,10] = 10\n", - " [1,11] = 11\n", - " [1,12] = 12\n", - " [1,13] = 13\n", - " [1,14] = 14\n", - " [1,15] = 15\n", - " [1,16] = 16\n", - " [1,17] = 17\n", - " [1,18] = 18\n", - " [1,19] = 19\n", - " [1,20] = 20\n", - " [1,21] = 21\n", - "}\n" - ] - } - ], - "source": [ - "bids.query(BIDS, 'subjects')" - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = {}(1x0)\n" - ] - } - ], - "source": [ - "bids.query(BIDS,'sessions')" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = \n", - "{\n", - " [1,1] = 01\n", - " [1,2] = 02\n", - "}\n" - ] - } - ], - "source": [ - "bids.query(BIDS, 'runs')" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = \n", - "{\n", - " [1,1] = Simontask\n", - "}\n" - ] - } - ], - "source": [ - "bids.query(BIDS, 'tasks')" - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = \n", - "{\n", - " [1,1] = T1w\n", - " [1,2] = bold\n", - " [1,3] = events\n", - "}\n" - ] - } - ], - "source": [ - "bids.query(BIDS, 'suffixes')" - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = \n", - "{\n", - " [1,1] = anat\n", - " [1,2] = func\n", - "}\n" - ] - } - ], - "source": [ - "bids.query(BIDS, 'modalities')" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = {}(1x0)\n" - ] - } - ], - "source": [ - "% Make more specific queries\n", - "bids.query(BIDS, 'runs', 'suffix', 'T1w')" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = \n", - "{\n", - " [1,1] = 01\n", - " [1,2] = 02\n", - "}\n" - ] - } - ], - "source": [ - "bids.query(BIDS, 'runs', 'suffix', 'bold')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Get filenames" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Get the NIfTI file for subject `'05'`, run `'02'` and task `'Simontask'`:" - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = \n", - "{\n", - " [1,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-05/func/sub-05_task-Simontask_run-02_bold.nii.gz\n", - "}\n" - ] - } - ], - "source": [ - "bids.query(BIDS,'data', 'sub','05', 'run', '02', 'task','Simontask', 'suffix','bold')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - " Note that for the entities listed below can be queried using integers:\n", - " - `'run'`\n", - " - `'flip'`\n", - " - `'inv'`\n", - " - `'split'`\n", - " - `'echo'`" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This can be also done by creating a structure that can be used as a library." - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "metadata": {}, - "outputs": [], - "source": [ - "filter = struct(...\n", - " 'sub','05', ...\n", - " 'run', 1:3, ...\n", - " 'task','Simontask', ...\n", - " 'suffix','bold');" - ] - }, - { - "cell_type": "code", - "execution_count": 40, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = \n", - "{\n", - " [1,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-05/func/sub-05_task-Simontask_run-01_bold.nii.gz\n", - " [2,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-05/func/sub-05_task-Simontask_run-02_bold.nii.gz\n", - "}\n" - ] - } - ], - "source": [ - "bids.query(BIDS, 'data', filter)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can also query data from several labels or indices" - ] - }, - { - "cell_type": "code", - "execution_count": 41, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "filter =\n", - "\n", - " scalar structure containing the fields:\n", - "\n", - " sub = \n", - " {\n", - " [1,1] = 01\n", - " [1,2] = 03\n", - " }\n", - " run =\n", - "\n", - " 1 2 3\n", - "\n", - " task = Simontask\n", - " suffix = bold\n", - "\n" - ] - } - ], - "source": [ - "filter.sub = {'01', '03'}" - ] - }, - { - "cell_type": "code", - "execution_count": 42, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = \n", - "{\n", - " [1,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-01/func/sub-01_task-Simontask_run-01_bold.nii.gz\n", - " [2,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-01/func/sub-01_task-Simontask_run-02_bold.nii.gz\n", - " [3,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-03/func/sub-03_task-Simontask_run-01_bold.nii.gz\n", - " [4,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-03/func/sub-03_task-Simontask_run-02_bold.nii.gz\n", - "}\n" - ] - } - ], - "source": [ - "bids.query(BIDS, 'data', filter)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Get metadata" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can also get the metadata of that file including TR:" - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = \n", - "{\n", - " [1,1] =\n", - "\n", - " scalar structure containing the fields:\n", - "\n", - " RepetitionTime = 2\n", - " TaskName = Simon task\n", - "\n", - " [1,2] =\n", - "\n", - " scalar structure containing the fields:\n", - "\n", - " RepetitionTime = 2\n", - " TaskName = Simon task\n", - "\n", - " [1,3] =\n", - "\n", - " scalar structure containing the fields:\n", - "\n", - " RepetitionTime = 2\n", - " TaskName = Simon task\n", - "\n", - " [1,4] =\n", - "\n", - " scalar structure containing the fields:\n", - "\n", - " RepetitionTime = 2\n", - " TaskName = Simon task\n", - "\n", - "}\n" - ] - } - ], - "source": [ - "bids.query(BIDS, 'metadata', filter)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Get the T1-weighted images from all subjects:" - ] - }, - { - "cell_type": "code", - "execution_count": 44, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = \n", - "{\n", - " [1,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-01/anat/sub-01_T1w.nii.gz\n", - " [2,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-02/anat/sub-02_T1w.nii.gz\n", - " [3,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-03/anat/sub-03_T1w.nii.gz\n", - " [4,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-04/anat/sub-04_T1w.nii.gz\n", - " [5,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-05/anat/sub-05_T1w.nii.gz\n", - " [6,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-06/anat/sub-06_T1w.nii.gz\n", - " [7,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-07/anat/sub-07_T1w.nii.gz\n", - " [8,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-08/anat/sub-08_T1w.nii.gz\n", - " [9,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-09/anat/sub-09_T1w.nii.gz\n", - " [10,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-10/anat/sub-10_T1w.nii.gz\n", - " [11,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-11/anat/sub-11_T1w.nii.gz\n", - " [12,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-12/anat/sub-12_T1w.nii.gz\n", - " [13,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-13/anat/sub-13_T1w.nii.gz\n", - " [14,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-14/anat/sub-14_T1w.nii.gz\n", - " [15,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-15/anat/sub-15_T1w.nii.gz\n", - " [16,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-16/anat/sub-16_T1w.nii.gz\n", - " [17,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-17/anat/sub-17_T1w.nii.gz\n", - " [18,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-18/anat/sub-18_T1w.nii.gz\n", - " [19,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-19/anat/sub-19_T1w.nii.gz\n", - " [20,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-20/anat/sub-20_T1w.nii.gz\n", - " [21,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-21/anat/sub-21_T1w.nii.gz\n", - "}\n" - ] - } - ], - "source": [ - "bids.query(BIDS, 'data', 'suffix', 'T1w')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Get \"dependencies\" of a given file" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This can be useful to find the files that are associated with the file you just queried.\n", - "\n", - "In this case the events file for a BOLD run." - ] - }, - { - "cell_type": "code", - "execution_count": 45, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "dependencies =\n", - "\n", - " scalar structure containing the fields:\n", - "\n", - " explicit = {}(0x0)\n", - " data = {}(0x0)\n", - " group = \n", - " {\n", - " [1,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-05/func/sub-05_task-Simontask_run-02_events.tsv\n", - " }\n", - "\n" - ] - } - ], - "source": [ - "filter = struct(...\n", - " 'sub','05', ...\n", - " 'run','02', ...\n", - " 'task','Simontask', ...\n", - " 'suffix','bold');\n", - " \n", - "dependencies = bids.query(BIDS, 'dependencies', filter) " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Using regular expressions" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "When using `bids.query` it is possible to use regular expressions. " - ] - }, - { - "cell_type": "code", - "execution_count": 46, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "data = \n", - "{\n", - " [1,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-01/func/sub-01_task-Simontask_run-02_bold.nii.gz\n", - " [2,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-02/func/sub-02_task-Simontask_run-02_bold.nii.gz\n", - " [3,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds101/sub-03/func/sub-03_task-Simontask_run-02_bold.nii.gz\n", - "}\n" - ] - } - ], - "source": [ - "filter = struct(...\n", - " 'sub','0[1-5]', ...\n", - " 'run','02', ...\n", - " 'task','Simon*', ...\n", - " 'suffix','bold');\n", - " \n", - "filter = struct(...\n", - " 'sub','0[1-3]', ...\n", - " 'run','02', ...\n", - " 'task','Sim.*', ...\n", - " 'suffix','bold'); \n", - " \n", - "data = bids.query(BIDS, 'data', filter) " - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Octave", - "language": "octave", - "name": "octave" - }, - "language_info": { - "file_extension": ".m", - "help_links": [ - { - "text": "GNU Octave", - "url": "https://www.gnu.org/software/octave/support.html" - }, - { - "text": "Octave Kernel", - "url": "https://github.com/Calysto/octave_kernel" - }, - { - "text": "MetaKernel Magics", - "url": "https://metakernel.readthedocs.io/en/latest/source/README.html" - } - ], - "mimetype": "text/x-octave", - "name": "octave", - "version": "4.2.2" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/examples/02_BIDS-Matlab_derivatives.ipynb b/examples/02_BIDS-Matlab_derivatives.ipynb deleted file mode 100644 index ce9ef55e..00000000 --- a/examples/02_BIDS-Matlab_derivatives.ipynb +++ /dev/null @@ -1,556 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "tags": [] - }, - "source": [ - "# BIDS-Matlab: derivatives\n", - "\n", - "1. [Indexing derivatives](#Indexing-derivatives)\n", - "1. [Indexing nested derivatives](#Indexing-nested-derivatives)\n", - "1. [Copying a raw dataset to start a new analysis](#Copying-a-raw-dataset-to-start-a-new-analysis)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "metadata": {}, - "outputs": [], - "source": [ - "% add bids-matlab to path\n", - "addpath(fullfile(pwd, '..'));" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Indexing derivatives\n", - "\n", - "Let's work on an `fmriprep` dataset.\n", - "\n", - "To work with derivatives data, we must ignore the BIDS schema for indexing." - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "use_schema = false();\n", - "\n", - "BIDS = bids.layout(fullfile(pwd, 'bids-examples', 'ds000001-fmriprep'), ...\n", - " 'use_schema', false);" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = \n", - "{\n", - " [1,1] = anat\n", - " [1,2] = figures\n", - " [1,3] = func\n", - "}\n" - ] - } - ], - "source": [ - "bids.query(BIDS, 'modalities')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The dataset description `DatasetType` confirms we are working with a derivative dataset." - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans =\n", - "\n", - " scalar structure containing the fields:\n", - "\n", - " Name = fMRIPrep - fMRI PREProcessing workflow\n", - " BIDSVersion = 1.4.0\n", - " DatasetType = derivative\n", - " GeneratedBy =\n", - "\n", - " scalar structure containing the fields:\n", - "\n", - " Name = fMRIPrep\n", - " Version = 20.2.0rc0\n", - " CodeURL = https://github.com/poldracklab/fmriprep/archive/20.2.0rc0.tar.gz\n", - "\n", - " HowToAcknowledge = Please cite our paper (https://doi.org/10.1038/s41592-018-0235-4), and include the generated citation boilerplate within the Methods section of the text.\n", - " License = This dataset is made available under the Public Domain Dedication and License \n", - "v1.0, whose full text can be found at \n", - "http://www.opendatacommons.org/licenses/pddl/1.0/. \n", - "We hope that all users will follow the ODC Attribution/Share-Alike \n", - "Community Norms (http://www.opendatacommons.org/norms/odc-by-sa/); \n", - "in particular, while not legally required, we hope that all users \n", - "of the data will acknowledge the OpenfMRI project and NSF Grant \n", - "OCI-1131441 (R. Poldrack, PI) in any publications.\n", - "\n" - ] - } - ], - "source": [ - "BIDS.description" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can access any preprocessed data by querying for data described (`desc` entity) as preprocessed (`preproc`) and maybe also in which `space` we want to work in." - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = \n", - "{\n", - " [1,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-10/anat/sub-10_space-MNI152NLin2009cAsym_res-2_desc-preproc_T1w.json\n", - " [2,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-10/anat/sub-10_space-MNI152NLin2009cAsym_res-2_desc-preproc_T1w.nii.gz\n", - " [3,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-11/anat/sub-11_space-MNI152NLin2009cAsym_res-2_desc-preproc_T1w.json\n", - " [4,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-11/anat/sub-11_space-MNI152NLin2009cAsym_res-2_desc-preproc_T1w.nii.gz\n", - " [5,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-13/anat/sub-13_space-MNI152NLin2009cAsym_res-2_desc-preproc_T1w.json\n", - " [6,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-13/anat/sub-13_space-MNI152NLin2009cAsym_res-2_desc-preproc_T1w.nii.gz\n", - " [7,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-16/anat/sub-16_space-MNI152NLin2009cAsym_res-2_desc-preproc_T1w.json\n", - " [8,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-16/anat/sub-16_space-MNI152NLin2009cAsym_res-2_desc-preproc_T1w.nii.gz\n", - "}\n" - ] - } - ], - "source": [ - "bids.query(BIDS, 'data', 'modality', 'anat', 'desc', 'preproc', 'space', 'MNI152NLin2009cAsym')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "But we can also get the surface data from Freesurfer.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = \n", - "{\n", - " [1,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-10/func/sub-10_task-balloonanalogrisktask_run-1_space-fsaverage5_hemi-L_bold.func.gii\n", - " [2,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-10/func/sub-10_task-balloonanalogrisktask_run-1_space-fsaverage5_hemi-R_bold.func.gii\n", - " [3,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-10/func/sub-10_task-balloonanalogrisktask_run-2_space-fsaverage5_hemi-L_bold.func.gii\n", - " [4,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-10/func/sub-10_task-balloonanalogrisktask_run-2_space-fsaverage5_hemi-R_bold.func.gii\n", - " [5,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-10/func/sub-10_task-balloonanalogrisktask_run-3_space-fsaverage5_hemi-L_bold.func.gii\n", - " [6,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-10/func/sub-10_task-balloonanalogrisktask_run-3_space-fsaverage5_hemi-R_bold.func.gii\n", - "}\n" - ] - } - ], - "source": [ - "bids.query(BIDS, 'data', 'sub', '10', 'modality', 'func', 'space', 'fsaverage5')" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = \n", - "{\n", - " [1,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-10/func/sub-10_task-balloonanalogrisktask_run-1_desc-confounds_timeseries.json\n", - " [2,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-10/func/sub-10_task-balloonanalogrisktask_run-1_desc-confounds_timeseries.tsv\n", - " [3,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-10/func/sub-10_task-balloonanalogrisktask_run-2_desc-confounds_timeseries.json\n", - " [4,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-10/func/sub-10_task-balloonanalogrisktask_run-2_desc-confounds_timeseries.tsv\n", - " [5,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-10/func/sub-10_task-balloonanalogrisktask_run-3_desc-confounds_timeseries.json\n", - " [6,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-10/func/sub-10_task-balloonanalogrisktask_run-3_desc-confounds_timeseries.tsv\n", - "}\n" - ] - } - ], - "source": [ - "bids.query(BIDS, 'data', 'sub', '10', 'desc', 'confounds')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can also directly look up json files when we don't use the BIDS schema." - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = \n", - "{\n", - " [1,1] = .csv\n", - " [1,2] = .func.gii\n", - " [1,3] = .h5\n", - " [1,4] = .html\n", - " [1,5] = .json\n", - " [1,6] = .nii.gz\n", - " [1,7] = .surf.gii\n", - " [1,8] = .svg\n", - " [1,9] = .tsv\n", - " [1,10] = .txt\n", - "}\n" - ] - } - ], - "source": [ - "bids.query(BIDS, 'extensions')" - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = \n", - "{\n", - " [1,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-10/func/sub-10_task-balloonanalogrisktask_run-3_space-MNI152NLin2009cAsym_res-2_desc-preproc_bold.json\n", - "}\n" - ] - } - ], - "source": [ - "filter.sub = '10';\n", - "filter.extension = '.json';\n", - "bids.query(BIDS, 'data', filter)" - ] - }, - { - "cell_type": "code", - "execution_count": 40, - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "json_file = \n", - "{\n", - " [1,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ds000001-fmriprep/sub-10/func/sub-10_task-balloonanalogrisktask_run-3_space-MNI152NLin2009cAsym_res-2_desc-preproc_bold.json\n", - "}\n", - "ans =\n", - "\n", - " scalar structure containing the fields:\n", - "\n", - " RepetitionTime = 2\n", - " SkullStripped = 0\n", - " TaskName = balloon analog risk task\n", - "\n" - ] - } - ], - "source": [ - "filter.space = 'MNI152NLin2009cAsym';\n", - "filter.desc = 'preproc';\n", - "filter.run = '3';\n", - "json_file = bids.query(BIDS, 'data', filter)\n", - "bids.util.jsondecode(json_file{1})" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Indexing nested derivatives" - ] - }, - { - "cell_type": "code", - "execution_count": 41, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "warning: OFF\n" - ] - } - ], - "source": [ - "warning('OFF')" - ] - }, - { - "cell_type": "code", - "execution_count": 42, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "warning: Duplicate key.\n", - "warning: called from\n", - " jsondecode at line 41 column 11\n", - " get_metadata at line 28 column 12\n", - " layout>manage_dependencies at line 547 column 14\n", - " layout at line 146 column 8\n", - " layout>index_derivatives_dir at line 187 column 43\n", - " layout at line 148 column 8\n", - "warning: Duplicate key.\n", - "warning: called from\n", - " jsondecode at line 41 column 11\n", - " get_metadata at line 28 column 12\n", - " layout>manage_dependencies at line 547 column 14\n", - " layout at line 146 column 8\n", - " layout>index_derivatives_dir at line 187 column 43\n", - " layout at line 148 column 8\n", - "warning: Duplicate key.\n", - "warning: called from\n", - " jsondecode at line 41 column 11\n", - " get_metadata at line 28 column 12\n", - " layout>manage_dependencies at line 547 column 14\n", - " layout at line 146 column 8\n", - " layout>index_derivatives_dir at line 187 column 43\n", - " layout at line 148 column 8\n", - "warning: Duplicate key.\n", - "warning: called from\n", - " jsondecode at line 41 column 11\n", - " get_metadata at line 28 column 12\n", - " layout>manage_dependencies at line 547 column 14\n", - " layout at line 146 column 8\n", - " layout>index_derivatives_dir at line 187 column 43\n", - " layout at line 148 column 8\n", - "warning: Duplicate key.\n", - "warning: called from\n", - " jsondecode at line 41 column 11\n", - " get_metadata at line 28 column 12\n", - " layout>manage_dependencies at line 547 column 14\n", - " layout at line 146 column 8\n", - " layout>index_derivatives_dir at line 187 column 43\n", - " layout at line 148 column 8\n", - "warning: Duplicate key.\n", - "warning: called from\n", - " jsondecode at line 41 column 11\n", - " get_metadata at line 28 column 12\n", - " layout>manage_dependencies at line 547 column 14\n", - " layout at line 146 column 8\n", - " layout>index_derivatives_dir at line 187 column 43\n", - " layout at line 148 column 8\n", - "warning: Duplicate key.\n", - "warning: called from\n", - " jsondecode at line 41 column 11\n", - " get_metadata at line 28 column 12\n", - " layout>manage_dependencies at line 547 column 14\n", - " layout at line 146 column 8\n", - " layout>index_derivatives_dir at line 187 column 43\n", - " layout at line 148 column 8\n" - ] - } - ], - "source": [ - "BIDS = bids.layout(fullfile(pwd,'bids-examples', 'ds000117'), ...\n", - " 'use_schema', false, ...\n", - " 'index_derivatives', true);" - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = \n", - "{\n", - " [1,1] = 01\n", - " [1,2] = 02\n", - " [1,3] = 03\n", - " [1,4] = 04\n", - " [1,5] = 05\n", - " [1,6] = 06\n", - " [1,7] = 07\n", - " [1,8] = 08\n", - " [1,9] = 09\n", - " [1,10] = 10\n", - " [1,11] = 11\n", - " [1,12] = 12\n", - " [1,13] = 13\n", - " [1,14] = 14\n", - " [1,15] = 15\n", - " [1,16] = 16\n", - " [1,17] = emptyroom\n", - "}\n" - ] - } - ], - "source": [ - "bids.query(BIDS.derivatives.meg_derivatives, 'subjects')" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "tags": [] - }, - "source": [ - "## Copying a raw dataset to start a new analysis\n", - "\n", - "Let's work on an `fmriprep` dataset.\n", - "\n", - "To work with derivatives data, we must ignore the BIDS schema for indexing." - ] - }, - { - "cell_type": "code", - "execution_count": 50, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "output_path = /home/remi/github/BIDS-matlab/examples/output\n", - "ans = 1\n", - "error: argument 'OUTPUT_PATH' is not a valid parameter\n", - "error: called from\n", - " error at line 539 column 7\n", - " parse at line 463 column 13\n", - " copy_to_derivative at line 78 column 3\n" - ] - } - ], - "source": [ - "dataset = fullfile(pwd, 'bids-examples', 'qmri_vfa');\n", - "\n", - "output_path = fullfile(pwd, 'output')\n", - "\n", - "filter = struct('modality', 'anat');\n", - "\n", - "pipeline_name = 'SPM12';\n", - "\n", - "bids.copy_to_derivative(dataset, ...\n", - " 'pipeline_name', pipeline_name, ...\n", - " 'output_path', output_path, ...\n", - " 'filter', filter, ...\n", - " 'force', true, ...\n", - " 'unzip', false, ...\n", - " 'verbose', true);" - ] - }, - { - "cell_type": "code", - "execution_count": 45, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "error: argument '/HOME/REMI/GITHUB/BIDS-MATLAB/EXAMPLES/OUTPUT/SPM12' is not a valid parameter\n", - "error: called from\n", - " error at line 539 column 7\n", - " parse at line 463 column 13\n", - " layout at line 59 column 3\n", - "error: structure has no member 'GeneratedBy'\n" - ] - } - ], - "source": [ - "BIDS = bids.layout(fullfile(output_path, 'SPM12'));\n", - "BIDS.description.GeneratedBy" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Octave", - "language": "octave", - "name": "octave" - }, - "language_info": { - "file_extension": ".m", - "help_links": [ - { - "text": "GNU Octave", - "url": "https://www.gnu.org/software/octave/support.html" - }, - { - "text": "Octave Kernel", - "url": "https://github.com/Calysto/octave_kernel" - }, - { - "text": "MetaKernel Magics", - "url": "https://metakernel.readthedocs.io/en/latest/source/README.html" - } - ], - "mimetype": "text/x-octave", - "name": "octave", - "version": "4.2.2" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/examples/03_BIDS-Matlab_filenames_and_metadata.ipynb b/examples/03_BIDS-Matlab_filenames_and_metadata.ipynb deleted file mode 100644 index 7b38a8a0..00000000 --- a/examples/03_BIDS-Matlab_filenames_and_metadata.ipynb +++ /dev/null @@ -1,516 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Create filenames, filepaths, and JSON\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "1. [Generating filenames](#Generating-filenames)\n", - "1. [Modifying filenames](#Modifying-filenames)\n", - "1. [Generating file names for derivatives](#Generating-file-names-for-derivatives)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "% add bids-matlab to path\n", - "addpath(fullfile(pwd, '..'));" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Generating filenames\n", - "\n", - "The vast majority of BIDS filenames have the following pattern:\n", - "\n", - "- a series of `entity-label` pairs separated by `_`\n", - "- a final `_suffix`\n", - "- a file `.extension`\n", - "- pseudo \"regular expression\" : `entity-label(_entity-label)+_suffix.extension`\n", - "\n", - "`entity`, `label`, `suffix`, `extension` are alphanumeric only (no special character): `([a-zA-Z0-9])+`\n", - "\n", - " - For example, suffixes can be `T1w` or `bold` but not `T1w_skullstripped` (no underscore allowed).\n", - "\n", - "Entity and label are separated by a dash: `entity-label --> ([a-zA-Z0-9])+-([a-zA-Z0-9])+`\n", - " \n", - " - For example, you can have: `sub-01` but not `sub-01-blind`\n", - "\n", - "Entity-label pairs are separated by an underscore:\n", - "\n", - " `entity-label(_entity-label)+ --> ([a-zA-Z0-9])+-([a-zA-Z0-9])+(_([a-zA-Z0-9])+-([a-zA-Z0-9])+)+`\n", - "\n", - "**Prefixes are not a thing in official BIDS names**\n", - "\n", - "\n", - "BIDS has a number of [officially recognised entities](https://bids-specification.readthedocs.io/en/stable/99-appendices/04-entity-table.html) \n", - "(`sub`, `ses`, `task`...) that must come in a specific order for each suffix.\n", - "\n", - "BIDS derivatives adds a few more entities (`desc`, `space`, `res`...) \n", - "and suffixes (`pseg`, `dseg`, `mask`...) that can be used to name and describe preprocessed data." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The `bids.File` class can help generate BIDS valid file names." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "input = struct('ext', '.nii');\n", - "input.suffix = 'bold';\n", - "input.entities = struct('sub', '01', ...\n", - " 'task', 'faceRecognition', ...\n", - " 'run', '02', ...\n", - " 'ses', 'test');" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = sub-01_task-faceRecognition_run-02_ses-test_bold.nii\n" - ] - } - ], - "source": [ - "file = bids.File(input);\n", - "\n", - "file.filename" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can rely on the BIDS schema to know in which order the entities must go for a certain `suffix` type. " - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = sub-01_ses-test_task-faceRecognition_run-02_bold.nii\n" - ] - } - ], - "source": [ - "file = bids.File(input, 'use_schema', true);\n", - "\n", - "file.filename" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This can also tell you if you are missing a required entity if you set `tolerant` to `false`." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "error: Entities 'task' cannot not be empty for the suffix 'bold'\n", - "error: called from\n", - " error_handling at line 44 column 5\n", - " bids_file_error at line 506 column 7\n", - " check_required_entities at line 476 column 9\n", - " update at line 276 column 7\n", - " reorder_entities at line 357 column 7\n", - " use_schema at line 379 column 11\n", - " File at line 146 column 13\n" - ] - } - ], - "source": [ - "input = struct('ext', '.nii');\n", - "input.suffix = 'bold';\n", - "input.entities = struct( ...\n", - " 'sub', '01', ...\n", - " 'ses', 'test', ...\n", - " 'run', '02');\n", - "\n", - "file = bids.File(input, 'use_schema', true, 'tolerant', false);\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Or you can specify the order of the entities manually." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = run-02_sub-01_ses-test_task-face recognition_bold.nii\n" - ] - } - ], - "source": [ - "input = struct('ext', '.nii');\n", - "input.suffix = 'bold';\n", - "input.entities = struct( ...\n", - " 'sub', '01', ...\n", - " 'task', 'face recognition', ...\n", - " 'run', '02', ...\n", - " 'ses', 'test');\n", - "file = bids.File(input);\n", - "\n", - "entity_order = {'run', 'sub', 'ses'};\n", - "\n", - "file = file.reorder_entities(entity_order);\n", - "file.filename\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Modifying filenames" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This can be used:\n", - "- to add, remove, modify any of the entities\n", - "- change the suffix\n", - "- change the extensions\n", - "- add or remove any prefix\n" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = sub-01_desc-brain_mask.nii\n" - ] - } - ], - "source": [ - "input = 'sub-01_ses-mri_T1w.nii';\n", - "file = bids.File(input, 'use_schema', false);\n", - "\n", - "file.suffix = 'mask';\n", - "file.entities.ses = '';\n", - "file.entities.desc = 'brain';\n", - "\n", - "file.filename" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Generating file names for derivatives" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This can also be useful to remove the prefix of some files." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = sub-01_ses-test_task-faceRecognition_run-02_space-IXI549Space_desc-preproc_bold.nii\n" - ] - } - ], - "source": [ - "input = 'wuasub-01_ses-test_task-faceRecognition_run-02_bold.nii';\n", - "\n", - "file = bids.File(input, 'use_schema', false);\n", - "file.prefix = '';\n", - "file.entities.space = 'IXI549Space';\n", - "file.entities.desc = 'preproc';\n", - "\n", - "file.filename" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This can prove useful to get a dummy json that should accompany any derivatives files." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = sub-01_ses-test_task-faceRecognition_run-02_space-IXI549Space_desc-preproc_bold.json\n", - "ans =\n", - "\n", - " scalar structure containing the fields:\n", - "\n", - " Description = RECOMMENDED\n", - " Sources = \n", - " {\n", - " [1,1] = \n", - " {\n", - " [1,1] = OPTIONAL\n", - " }\n", - " }\n", - " RawSources = \n", - " {\n", - " [1,1] = \n", - " {\n", - " [1,1] = OPTIONAL\n", - " }\n", - " }\n", - " SpatialReference = \n", - " {\n", - " [1,1] = \n", - " {\n", - " [1,1] = REQUIRED if no space entity or if non standard space RECOMMENDED otherwise\n", - " }\n", - " }\n", - "\n" - ] - } - ], - "source": [ - "json = bids.derivatives_json(file.filename);\n", - "\n", - "json.filename\n", - "json.content" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The content of the JSON should adapt depending on the entities or suffix present in the output filename." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "json =\n", - "\n", - " scalar structure containing the fields:\n", - "\n", - " filename = sub-01_ses-test_task-faceRecognition_res-2pt0_space-IXI549Space_desc-brain_mask.json\n", - " content =\n", - "\n", - " scalar structure containing the fields:\n", - "\n", - " Description = RECOMMENDED\n", - " Sources = \n", - " {\n", - " [1,1] = \n", - " {\n", - " [1,1] = OPTIONAL\n", - " }\n", - " }\n", - " RawSources = \n", - " {\n", - " [1,1] = \n", - " {\n", - " [1,1] = REQUIRED\n", - " }\n", - " }\n", - " SpatialReference = \n", - " {\n", - " [1,1] = \n", - " {\n", - " [1,1] = REQUIRED if no space entity or if non standard space RECOMMENDED otherwise\n", - " }\n", - " }\n", - " Resolution = \n", - " {\n", - " [1,1] = \n", - " {\n", - " [1,1] =\n", - "\n", - " scalar structure containing the fields:\n", - "\n", - " 2pt0: 1x24 sq_string\n", - "\n", - " }\n", - " }\n", - " Atlas = \n", - " {\n", - " [1,1] = \n", - " {\n", - " [1,1] = OPTIONAL\n", - " }\n", - " }\n", - " Type = \n", - " {\n", - " [1,1] = \n", - " {\n", - " [1,1] = OPTIONAL\n", - " }\n", - " }\n", - "\n", - "\n", - "ans = sub-01_ses-test_task-faceRecognition_res-2pt0_space-IXI549Space_desc-brain_mask.json\n", - "ans =\n", - "\n", - " scalar structure containing the fields:\n", - "\n", - " Description = RECOMMENDED\n", - " Sources = \n", - " {\n", - " [1,1] = \n", - " {\n", - " [1,1] = OPTIONAL\n", - " }\n", - " }\n", - " RawSources = \n", - " {\n", - " [1,1] = \n", - " {\n", - " [1,1] = REQUIRED\n", - " }\n", - " }\n", - " SpatialReference = \n", - " {\n", - " [1,1] = \n", - " {\n", - " [1,1] = REQUIRED if no space entity or if non standard space RECOMMENDED otherwise\n", - " }\n", - " }\n", - " Resolution = \n", - " {\n", - " [1,1] = \n", - " {\n", - " [1,1] =\n", - "\n", - " scalar structure containing the fields:\n", - "\n", - " 2pt0 = REQUIRED if \"res\" entity\n", - "\n", - " }\n", - " }\n", - " Atlas = \n", - " {\n", - " [1,1] = \n", - " {\n", - " [1,1] = OPTIONAL\n", - " }\n", - " }\n", - " Type = \n", - " {\n", - " [1,1] = \n", - " {\n", - " [1,1] = OPTIONAL\n", - " }\n", - " }\n", - "\n", - "ans =\n", - "\n", - " scalar structure containing the fields:\n", - "\n", - " 2pt0 = REQUIRED if \"res\" entity\n", - "\n" - ] - } - ], - "source": [ - "json = bids.derivatives_json('sub-01_ses-test_task-faceRecognition_res-2pt0_space-IXI549Space_desc-brain_mask.nii')\n", - "json.filename\n", - "json.content\n", - "json.content.Resolution{1}{1}" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Octave", - "language": "octave", - "name": "octave" - }, - "language_info": { - "file_extension": ".m", - "help_links": [ - { - "text": "GNU Octave", - "url": "https://www.gnu.org/software/octave/support.html" - }, - { - "text": "Octave Kernel", - "url": "https://github.com/Calysto/octave_kernel" - }, - { - "text": "MetaKernel Magics", - "url": "https://metakernel.readthedocs.io/en/latest/source/README.html" - } - ], - "mimetype": "text/x-octave", - "name": "octave", - "version": "4.2.2" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/examples/04_BIDS-Matlab_new_datasets.ipynb b/examples/04_BIDS-Matlab_new_datasets.ipynb deleted file mode 100644 index 73a12822..00000000 --- a/examples/04_BIDS-Matlab_new_datasets.ipynb +++ /dev/null @@ -1,489 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Create filenames, filepaths, and JSON\n" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [], - "source": [ - "% add bids-matlab to path\n", - "addpath(fullfile(pwd, '..'));" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Initialising a new BIDS dataset" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This can be useful when you are going to output your analysis or your data acquisition into a new dataset." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - " Initialize dataset with README, description, folder structure...\n", - "\n", - " USAGE::\n", - "\n", - " bids.init(pth, ...\n", - " 'folders', folders, ,...\n", - " 'is_derivative', false,...\n", - " 'is_datalad_ds', false)\n", - "\n", - " :param pth: directory where to create the dataset\n", - " :type pth: string\n", - "\n", - " :param folders: define the folder structure to create.\n", - " ``folders.subjects``\n", - " ``folders.sessions``\n", - " ``folders.modalities``\n", - " :type folders: structure\n", - "\n", - " :param is_derivative:\n", - " :type is_derivative: boolean\n", - "\n", - " :param is_datalad_ds:\n", - " :type is_derivative: boolean\n", - "\n", - "\n", - " (C) Copyright 2021 BIDS-MATLAB developers\n", - "\n", - "\n", - "Additional help for built-in functions and operators is\n", - "available in the online version of the manual. Use the command\n", - "'doc ' to search the manual index.\n", - "\n", - "Help and information about Octave is also available on the WWW\n", - "at http://www.octave.org and via the help@octave.org\n", - "mailing list.\n" - ] - } - ], - "source": [ - "help bids.init" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Derivatives datasets have some extra info in their `dataset_description.json`.\n", - "\n", - "If you are going to curate the dataset with [Datalad](http://handbook.datalad.org/en/latest/), you can also mention it and this will modify the README to add extra info about this (taken from the datalad handbook)." - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [], - "source": [ - "pth = fullfile(pwd, 'dummy_ds');\n", - "\n", - "folders.subjects = {'01', '02'};\n", - "folders.sessions = {'pre', 'post'};\n", - "folders.modalities = {'anat', 'eeg'};" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [], - "source": [ - "bids.init(pth, 'folders', folders, 'is_derivative', true, 'is_datalad_ds', true)" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[01;34mdummy_ds\u001b[00m\n", - "├── CHANGES\n", - "├── dataset_description.json\n", - "├── README\n", - "├── \u001b[01;34msub-01\u001b[00m\n", - "│   ├── \u001b[01;34mses-post\u001b[00m\n", - "│   │   ├── \u001b[01;34manat\u001b[00m\n", - "│   │   └── \u001b[01;34meeg\u001b[00m\n", - "│   └── \u001b[01;34mses-pre\u001b[00m\n", - "│   ├── \u001b[01;34manat\u001b[00m\n", - "│   └── \u001b[01;34meeg\u001b[00m\n", - "└── \u001b[01;34msub-02\u001b[00m\n", - " ├── \u001b[01;34mses-post\u001b[00m\n", - " │   ├── \u001b[01;34manat\u001b[00m\n", - " │   └── \u001b[01;34meeg\u001b[00m\n", - " └── \u001b[01;34mses-pre\u001b[00m\n", - " ├── \u001b[01;34manat\u001b[00m\n", - " └── \u001b[01;34meeg\u001b[00m\n", - "\n", - "14 directories, 3 files\n", - "\n" - ] - } - ], - "source": [ - "!tree dummy_ds" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Template README was generated." - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "# README\n", - "\n", - "The README is usually the starting point for researchers using your data\n", - "and serves as a guidepost for users of your data. A clear and informative\n", - "README makes your data much more usable.\n", - "\n", - "In general you can include information in the README that is not captured by some other\n", - "files in the BIDS dataset (dataset_description.json, events.tsv, ...).\n", - "\n", - "It can also be useful to also include information that might already be\n", - "present in another file of the dataset but might be important for users to be aware of\n", - "before preprocessing or analysing the data.\n", - "\n", - "If the README gets too long you have the possibility to create a `/doc` folder\n", - "and add it to the `.bidsignore` file to make sure it is ignored by the BIDS validator.\n", - "\n", - "More info here: https://neurostars.org/t/where-in-a-bids-dataset-should-i-put-notes-about-individual-mri-acqusitions/17315/3\n", - "\n", - "## Details related to access to the data\n", - "\n", - "- [ ] Data user agreement\n", - "\n", - "If the dataset requires a data user agreement, link to the relevant information.\n", - "\n", - "- [ ] Contact person\n", - "\n", - "Indicate the name and contact details (email and ORCID) of the person responsible for additional information.\n", - "\n", - "- [ ] Practical information to access the data\n", - "\n", - "If there is any special information related to access rights or\n", - "how to download the data make sure to include it.\n", - "For example, if the dataset was curated using datalad,\n", - "make sure to include the relevant section from the datalad handbook:\n", - "http://handbook.datalad.org/en/latest/basics/101-180-FAQ.html#how-can-i-help-others-get-started-with-a-shared-dataset\n", - "\n", - "## Overview\n", - "\n", - "- [ ] Project name (if relevant)\n", - "\n", - "- [ ] Year(s) that the project ran\n", - "\n", - "If no `scans.tsv` is included, this could at least cover when the data acquisition\n", - "starter and ended. Local time of day is particularly relevant to subject state.\n", - "\n", - "- [ ] Brief overview of the tasks in the experiment\n", - "\n", - "A paragraph giving an overview of the experiment. This should include the\n", - "goals or purpose and a discussion about how the experiment tries to achieve\n", - "these goals.\n", - "\n", - "- [ ] Description of the contents of the dataset\n", - "\n", - "An easy thing to add is the output of the bids-validator that describes what type of\n", - "data and the number of subject one can expect to find in the dataset.\n", - "\n", - "- [ ] Independent variables\n", - "\n", - "A brief discussion of condition variables (sometimes called contrasts\n", - "or independent variables) that were varied across the experiment.\n", - "\n", - "- [ ] Dependent variables\n", - "\n", - "A brief discussion of the response variables (sometimes called the\n", - "dependent variables) that were measured and or calculated to assess\n", - "the effects of varying the condition variables. This might also include\n", - "questionnaires administered to assess behavioral aspects of the experiment.\n", - "\n", - "- [ ] Control variables\n", - "\n", - "A brief discussion of the control variables --- that is what aspects\n", - "were explicitly controlled in this experiment. The control variables might\n", - "include subject pool, environmental conditions, set up, or other things\n", - "that were explicitly controlled.\n", - "\n", - "- [ ] Quality assessment of the data\n", - "\n", - "Provide a short summary of the quality of the data ideally with descriptive statistics if relevant\n", - "and with a link to more comprehensive description (like with MRIQC) if possible.\n", - "\n", - "## Methods\n", - "\n", - "### Subjects\n", - "\n", - "A brief sentence about the subject pool in this experiment.\n", - "\n", - "Remember that `Control` or `Patient` status should be defined in the `participants.tsv`\n", - "using a group column.\n", - "\n", - "- [ ] Information about the recruitment procedure\n", - "- [ ] Subject inclusion criteria (if relevant)\n", - "- [ ] Subject exclusion criteria (if relevant)\n", - "\n", - "### Apparatus\n", - "\n", - "A summary of the equipment and environment setup for the\n", - "experiment. For example, was the experiment performed in a shielded room\n", - "with the subject seated in a fixed position.\n", - "\n", - "### Initial setup\n", - "\n", - "A summary of what setup was performed when a subject arrived.\n", - "\n", - "### Task organization\n", - "\n", - "How the tasks were organized for a session.\n", - "This is particularly important because BIDS datasets usually have task data\n", - "separated into different files.)\n", - "\n", - "- [ ] Was task order counter-balanced?\n", - "- [ ] What other activities were interspersed between tasks?\n", - "\n", - "- [ ] In what order were the tasks and other activities performed?\n", - "\n", - "### Task details\n", - "\n", - "As much detail as possible about the task and the events that were recorded.\n", - "\n", - "### Additional data acquired\n", - "\n", - "A brief indication of data other than the\n", - "imaging data that was acquired as part of this experiment. In addition\n", - "to data from other modalities and behavioral data, this might include\n", - "questionnaires and surveys, swabs, and clinical information. Indicate\n", - "the availability of this data.\n", - "\n", - "This is especially relevant if the data are not included in a `phenotype` folder.\n", - "https://bids-specification.readthedocs.io/en/stable/03-modality-agnostic-files.html#phenotypic-and-assessment-data\n", - "\n", - "### Experimental location\n", - "\n", - "This should include any additional information regarding the\n", - "the geographical location and facility that cannot be included\n", - "in the relevant json files.\n", - "\n", - "### Missing data\n", - "\n", - "Mention something if some participants are missing some aspects of the data.\n", - "This can take the form of a processing log and/or abnormalities about the dataset.\n", - "\n", - "Some examples:\n", - "\n", - "- A brain lesion or defect only present in one participant\n", - "- Some experimental conditions missing on a given run for a participant because\n", - " of some technical issue.\n", - "- Any noticeable feature of the data for certain participants\n", - "- Differences (even slight) in protocol for certain participants.\n", - "\n", - "### Notes\n", - "\n", - "Any additional information or pointers to information that\n", - "might be helpful to users of the dataset. Include qualitative information\n", - "related to how the data acquisition went.\n", - "________________________________________________________________________________\n", - "\n", - "[![made-with-datalad](https://www.datalad.org/badges/made_with.svg)](https://datalad.org)\n", - "\n", - "## DataLad datasets and how to use them\n", - "\n", - "This repository is a [DataLad](https://www.datalad.org/) dataset. It provides\n", - "fine-grained data access down to the level of individual files, and allows for\n", - "tracking future updates. In order to use this repository for data retrieval,\n", - "[DataLad](https://www.datalad.org/) is required. It is a free and\n", - "open source command line tool, available for all major operating\n", - "systems, and builds up on Git and [git-annex](https://git-annex.branchable.com/)\n", - "to allow sharing, synchronizing, and version controlling collections of\n", - "large files. You can find information on how to install DataLad at\n", - "[handbook.datalad.org/en/latest/intro/installation.html](http://handbook.datalad.org/en/latest/intro/installation.html).\n", - "\n", - "### Get the dataset\n", - "\n", - "A DataLad dataset can be `cloned` by running\n", - "\n", - "```\n", - "datalad clone \n", - "```\n", - "\n", - "Once a dataset is cloned, it is a light-weight directory on your local machine.\n", - "At this point, it contains only small metadata and information on the\n", - "identity of the files in the dataset, but not actual *content* of the\n", - "(sometimes large) data files.\n", - "\n", - "### Retrieve dataset content\n", - "\n", - "After cloning a dataset, you can retrieve file contents by running\n", - "\n", - "```\n", - "datalad get `\n", - "```\n", - "\n", - "This command will trigger a download of the files, directories, or\n", - "subdatasets you have specified.\n", - "\n", - "DataLad datasets can contain other datasets, so called *subdatasets*.\n", - "If you clone the top-level dataset, subdatasets do not yet contain\n", - "metadata and information on the identity of files, but appear to be\n", - "empty directories. In order to retrieve file availability metadata in\n", - "subdatasets, run\n", - "\n", - "```\n", - "datalad get -n \n", - "```\n", - "\n", - "Afterwards, you can browse the retrieved metadata to find out about\n", - "subdataset contents, and retrieve individual files with `datalad get`.\n", - "If you use `datalad get `, all contents of the\n", - "subdataset will be downloaded at once.\n", - "\n", - "### Stay up-to-date\n", - "\n", - "DataLad datasets can be updated. The command `datalad update` will\n", - "*fetch* updates and store them on a different branch (by default\n", - "`remotes/origin/master`). Running\n", - "\n", - "```\n", - "datalad update --merge\n", - "```\n", - "\n", - "will *pull* available updates and integrate them in one go.\n", - "\n", - "### Find out what has been done\n", - "\n", - "DataLad datasets contain their history in the ``git log``.\n", - "By running ``git log`` (or a tool that displays Git history) in the dataset or on\n", - "specific files, you can find out what has been done to the dataset or to individual files\n", - "by whom, and when.\n", - "\n", - "### More information\n", - "\n", - "More information on DataLad and how to use it can be found in the DataLad Handbook at\n", - "[handbook.datalad.org](http://handbook.datalad.org/en/latest/index.html). The chapter\n", - "\"DataLad datasets\" can help you to familiarize yourself with the concept of a dataset.\n", - "\n" - ] - } - ], - "source": [ - "!cat dummy_ds/README" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\n", - " \"Name\": \"\",\n", - " \"BIDSVersion\": \"\",\n", - " \"DatasetType\": \"derivative\",\n", - " \"License\": \"\",\n", - " \"Acknowledgements\": \"\",\n", - " \"HowToAcknowledge\": \"\",\n", - " \"DatasetDOI\": \"\",\n", - " \"HEDVersion\": \"\",\n", - " \"Funding\": [],\n", - " \"Authors\": [],\n", - " \"ReferencesAndLinks\": [],\n", - " \"GeneratedBy\": [\n", - " {\n", - " \"Name\": \"\",\n", - " \"Version\": \"\",\n", - " \"Description\": \"\",\n", - " \"CodeURL\": \"\",\n", - " \"Container\": {\n", - " \"Type\": \"\",\n", - " \"Tag\": \"\"\n", - " }\n", - " }\n", - " ],\n", - " \"SourceDatasets\": [\n", - " {\n", - " \"DOI\": \"\",\n", - " \"URL\": \"\",\n", - " \"Version\": \"\"\n", - " }\n", - " ]\n", - "}\n" - ] - } - ], - "source": [ - "!cat dummy_ds/dataset_description.json" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Octave", - "language": "octave", - "name": "octave" - }, - "language_info": { - "file_extension": ".m", - "help_links": [ - { - "text": "GNU Octave", - "url": "https://www.gnu.org/software/octave/support.html" - }, - { - "text": "Octave Kernel", - "url": "https://github.com/Calysto/octave_kernel" - }, - { - "text": "MetaKernel Magics", - "url": "https://metakernel.readthedocs.io/en/latest/source/README.html" - } - ], - "mimetype": "text/x-octave", - "name": "octave", - "version": "4.2.2" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/examples/05_BIDS-Matlab_report.ipynb b/examples/05_BIDS-Matlab_report.ipynb deleted file mode 100644 index 1cc03e5c..00000000 --- a/examples/05_BIDS-Matlab_report.ipynb +++ /dev/null @@ -1,194 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# BIDS-matlab: reports" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "% add bids-matlab to path\n", - "addpath(fullfile(pwd, '..'));" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "BIDS = bids.layout(fullfile(pwd, 'bids-examples', 'ds101'));" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Dataset description saved in: /home/remi/github/BIDS-matlab/examples/output/dataset-ds101_bids-matlab_report.md\n", - "\n", - "\n", - "\n", - "\n", - "--------------------------------------------------------------------------------\n", - "warning: saving to file not implement for Ocatve\n", - "warning: called from\n", - " error_handling at line 48 column 5\n", - " report>print_to_output at line 592 column 7\n", - " report at line 102 column 9\n", - "ANAT REPORT\n", - "\n", - "\n", - " Getting parameters - sub-01_T1w.nii.gz\n", - "\n", - "warning: saving to file not implement for Ocatve\n", - "warning: called from\n", - " error_handling at line 48 column 5\n", - " report>print_to_output at line 592 column 7\n", - " report>print_text at line 659 column 3\n", - " report>report_nifti at line 187 column 5\n", - " report at line 109 column 13\n", - "The data acquisition was performed in the {{InstitutionName}}, {{InstitutionalDepartmentName}}, \n", - "{{InstitutionAddress}}.\n", - "\n", - "warning: saving to file not implement for Ocatve\n", - "warning: called from\n", - " error_handling at line 48 column 5\n", - " report>print_to_output at line 592 column 7\n", - " report>print_text at line 659 column 3\n", - " report>report_nifti at line 193 column 7\n", - " report at line 109 column 13\n", - "MRI scans were acquired at {{MagneticFieldStrength}} Tesla using a {{ManufacturersModelName}} \n", - "system from {{Manufacturer}}, with serial number {{DeviceSerialNumber}}. \n", - "The software version was {{SoftwareVersions}}.\n", - "\n", - "warning: saving to file not implement for Ocatve\n", - "warning: called from\n", - " error_handling at line 48 column 5\n", - " report>print_to_output at line 592 column 7\n", - " report>report_nifti at line 196 column 5\n", - " report at line 109 column 13\n", - "{{PulseSequenceType}} {{ScanningSequence}} {{SequenceVariant}} T1w structural MRI \n", - "data were collected ({{nb_slices}} slices; repetition time, TR= {{RepetitionTime}} \n", - "s; echo time, TE= {{echo_time}} ms; flip angle, FA= {{FlipAngle}} deg; field \n", - "of view, FOV= {{fov}} mm; matrix size= {{mat_size}}; voxel size= {{vox_size}} \n", - "mm).\n", - "\n", - "warning: saving to file not implement for Ocatve\n", - "warning: called from\n", - " error_handling at line 48 column 5\n", - " report>print_to_output at line 592 column 7\n", - " report at line 102 column 9\n", - "FUNC REPORT\n", - "\n", - "\n", - " Getting parameters - sub-01_task-Simontask_run-01_bold.nii.gz\n", - "\n", - "warning: saving to file not implement for Ocatve\n", - "warning: called from\n", - " error_handling at line 48 column 5\n", - " report>print_to_output at line 592 column 7\n", - " report>print_text at line 659 column 3\n", - " report>report_func at line 239 column 7\n", - " report at line 112 column 13\n", - "The data acquisition was performed in the {{InstitutionName}}, {{InstitutionalDepartmentName}}, \n", - "{{InstitutionAddress}}.\n", - "\n", - "warning: saving to file not implement for Ocatve\n", - "warning: called from\n", - " error_handling at line 48 column 5\n", - " report>print_to_output at line 592 column 7\n", - " report>print_text at line 659 column 3\n", - " report>report_func at line 241 column 7\n", - " report at line 112 column 13\n", - "MRI scans were acquired at {{MagneticFieldStrength}} Tesla using a {{ManufacturersModelName}} \n", - "system from {{Manufacturer}}, with serial number {{DeviceSerialNumber}}. \n", - "The software version was {{SoftwareVersions}}.\n", - "\n", - "warning: saving to file not implement for Ocatve\n", - "warning: called from\n", - " error_handling at line 48 column 5\n", - " report>print_to_output at line 592 column 7\n", - " report>report_func at line 243 column 7\n", - " report at line 112 column 13\n", - "For the Simon task task, 2 run(s) of bold {{PulseSequenceType}} {{ScanningSequence}} \n", - "{{SequenceVariant}} fMRI data were collected. The acquisition parameters were: \n", - "{{nb_slices}} slices acquired in a {{so_str}} fashion; repetition time, TR= 2 \n", - "s; echo time, TE= {{echo_time}} ms; flip angle, FA= {{FlipAngle}} deg; field of \n", - "view, FOV= {{fov}} mm; matrix size= {{mat_size}}; voxel size= {{vox_size}} mm; \n", - "multiband factor= {{mb_str}}; in-plane acceleration factor= {pr_str}}. Each run \n", - "was {{length}} minutes in length, during which {{nb_vols}} functional volumes were \n", - "acquired.\n", - "\n", - "warning: saving to file not implement for Ocatve\n", - "warning: called from\n", - " error_handling at line 48 column 5\n", - " report>print_to_output at line 592 column 7\n", - " report>print_text at line 659 column 3\n", - " report>report_func at line 245 column 7\n", - " report at line 112 column 13\n", - "{{TaskDescription}} Participants were specifically instructed to: {{Instructions}}\n", - "\n", - "warning: saving to file not implement for Ocatve\n", - "warning: called from\n", - " error_handling at line 48 column 5\n", - " report>print_to_output at line 592 column 7\n", - " report>print_text at line 659 column 3\n", - " report at line 131 column 3\n", - "This text was automatically generated by [BIDS-matlab](https://github.com/bids-standard/bids-matlab).\n", - "\n" - ] - } - ], - "source": [ - "read_nifti = false;\n", - "output_path = fullfile(pwd, 'output');\n", - "verbose = true;\n", - "\n", - "bids.report(BIDS,\n", - " 'output_path', output_path, ...\n", - " 'read_nifti', read_nifti, ...\n", - " 'verbose', verbose);" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Octave", - "language": "octave", - "name": "octave" - }, - "language_info": { - "file_extension": ".m", - "help_links": [ - { - "text": "GNU Octave", - "url": "https://www.gnu.org/software/octave/support.html" - }, - { - "text": "Octave Kernel", - "url": "https://github.com/Calysto/octave_kernel" - }, - { - "text": "MetaKernel Magics", - "url": "https://metakernel.readthedocs.io/en/latest/source/README.html" - } - ], - "mimetype": "text/x-octave", - "name": "octave", - "version": "4.2.2" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/examples/06_BIDS-Matlab_tsv_json.ipynb b/examples/06_BIDS-Matlab_tsv_json.ipynb deleted file mode 100644 index 75625543..00000000 --- a/examples/06_BIDS-Matlab_tsv_json.ipynb +++ /dev/null @@ -1,1968 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "6d502f01", - "metadata": {}, - "source": [ - "# BIDS-Matlab: TSV and JSON files\n", - "\n", - "1. [Read from TSV files](#Read-from-TSV-files)\n", - "1. [Write to TSV files](#Write-to-TSV-files)\n", - "1. [Write to JSON files](#Write-to-JSON-files)\n", - "1. [Read from JSON files](#Read-from-JSON-files)\n", - "\n", - "\n", - "## Read from TSV files\n", - "\n", - "This can be done with the `bids.util.tsvread` function." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "92b7a41a", - "metadata": {}, - "outputs": [], - "source": [ - "% add bids-matlab to path\n", - "addpath(fullfile(pwd, '..'));\n", - "\n", - "warning('off','all');" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "1ec10866", - "metadata": {}, - "outputs": [], - "source": [ - "BIDS = bids.layout(fullfile(pwd,'bids-examples','ieeg_visual'));" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "59ed8d9a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans = \n", - "{\n", - " [1,1] = 01\n", - " [1,2] = 02\n", - "}\n", - "ans = \n", - "{\n", - " [1,1] = visual\n", - "}\n", - "events_file = \n", - "{\n", - " [1,1] = /home/remi/github/BIDS-matlab/examples/bids-examples/ieeg_visual/sub-01/ses-01/ieeg/sub-01_ses-01_task-visual_run-01_events.tsv\n", - "}\n" - ] - } - ], - "source": [ - "bids.query(BIDS, 'subjects') \n", - "bids.query(BIDS, 'tasks')\n", - "events_file = bids.query(BIDS, 'data', 'sub', '01', 'task', 'visual', 'suffix', 'events')" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "8d605782", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans =\n", - "\n", - " scalar structure containing the fields:\n", - "\n", - " onset =\n", - "\n", - " 17.853\n", - " 18.356\n", - " 18.858\n", - " 19.360\n", - " 19.862\n", - " 20.364\n", - " 20.867\n", - " 21.369\n", - " 21.871\n", - " 22.373\n", - " 22.875\n", - " 23.378\n", - " 23.880\n", - " 24.382\n", - " 24.884\n", - " 25.386\n", - " 25.888\n", - " 26.391\n", - " 26.893\n", - " 27.395\n", - " 27.897\n", - " 28.399\n", - " 28.902\n", - " 29.404\n", - " 29.906\n", - " 30.408\n", - " 30.910\n", - " 31.413\n", - " 31.915\n", - " 32.417\n", - " 32.919\n", - " 33.421\n", - " 33.924\n", - " 34.426\n", - " 34.928\n", - " 35.430\n", - " 35.932\n", - " 36.435\n", - " 36.937\n", - " 37.439\n", - " 37.941\n", - " 38.443\n", - " 38.945\n", - " 39.448\n", - " 39.950\n", - " 40.452\n", - " 40.954\n", - " 41.456\n", - " 41.959\n", - " 42.461\n", - " 42.963\n", - " 43.465\n", - " 43.967\n", - " 44.470\n", - " 44.972\n", - " 45.474\n", - " 45.976\n", - " 46.478\n", - " 46.981\n", - " 47.483\n", - " 47.985\n", - " 48.487\n", - " 48.989\n", - " 49.492\n", - " 49.994\n", - " 50.496\n", - " 50.998\n", - " 51.500\n", - " 52.003\n", - " 52.505\n", - " 53.007\n", - " 53.509\n", - " 54.011\n", - " 54.513\n", - " 55.016\n", - " 55.518\n", - " 56.020\n", - " 56.522\n", - " 57.024\n", - " 57.527\n", - " 58.029\n", - " 58.531\n", - " 59.033\n", - " 59.535\n", - " 60.038\n", - " 60.540\n", - " 61.042\n", - " 61.544\n", - " 62.046\n", - " 62.549\n", - " 63.051\n", - " 63.553\n", - " 64.055\n", - " 64.558\n", - " 65.060\n", - " 65.562\n", - " 66.064\n", - " 66.566\n", - " 67.069\n", - " 67.571\n", - " 68.073\n", - " 68.575\n", - " 69.077\n", - " 69.580\n", - " 70.082\n", - " 70.584\n", - " 71.086\n", - " 71.588\n", - " 72.091\n", - " 72.593\n", - " 73.095\n", - " 73.597\n", - " 74.099\n", - " 74.602\n", - " 75.104\n", - " 75.606\n", - " 76.108\n", - " 76.610\n", - " 77.112\n", - " 77.615\n", - " 78.117\n", - " 78.619\n", - " 79.121\n", - " 79.623\n", - " 80.126\n", - " 80.628\n", - " 81.130\n", - " 81.632\n", - " 82.134\n", - " 82.637\n", - " 83.139\n", - " 83.641\n", - " 84.143\n", - " 84.645\n", - " 85.148\n", - " 85.650\n", - " 86.152\n", - " 86.654\n", - " 87.156\n", - " 87.659\n", - " 88.161\n", - " 88.663\n", - " 89.165\n", - " 89.667\n", - " 90.169\n", - " 90.672\n", - " 91.174\n", - " 91.676\n", - " 92.178\n", - " 92.680\n", - " 93.183\n", - " 93.685\n", - " 94.187\n", - " 94.689\n", - " 95.191\n", - " 95.694\n", - " 96.196\n", - " 96.698\n", - " 97.200\n", - " 97.702\n", - " 98.205\n", - " 98.707\n", - " 99.209\n", - " 99.711\n", - " 100.213\n", - " 100.716\n", - " 101.218\n", - " 101.720\n", - " 102.222\n", - " 102.724\n", - " 103.226\n", - " 103.729\n", - " 104.231\n", - " 104.733\n", - " 105.235\n", - " 105.737\n", - " 106.240\n", - " 106.742\n", - " 107.244\n", - " 107.746\n", - " 108.248\n", - " 108.751\n", - " 109.253\n", - " 109.755\n", - " 110.257\n", - " 110.759\n", - " 111.262\n", - " 111.764\n", - " 112.266\n", - " 112.768\n", - " 113.270\n", - " 113.773\n", - " 114.275\n", - " 114.777\n", - " 115.279\n", - " 115.781\n", - " 116.283\n", - " 116.786\n", - " 117.288\n", - " 117.790\n", - " 118.292\n", - " 118.794\n", - " 119.297\n", - " 119.799\n", - " 120.301\n", - " 120.803\n", - " 121.305\n", - " 121.808\n", - " 122.310\n", - " 122.812\n", - " 123.314\n", - " 123.816\n", - " 124.319\n", - " 124.821\n", - " 125.323\n", - " 125.825\n", - " 126.327\n", - " 126.830\n", - " 127.332\n", - " 127.834\n", - " 128.336\n", - " 128.838\n", - " 129.340\n", - " 129.843\n", - " 130.345\n", - " 130.847\n", - " 131.350\n", - " 131.851\n", - " 132.354\n", - " 132.856\n", - " 133.358\n", - " 133.861\n", - " 134.363\n", - " 134.865\n", - " 135.367\n", - " 135.869\n", - " 136.371\n", - " 136.874\n", - " 137.376\n", - " 137.878\n", - " 138.380\n", - " 138.882\n", - " 139.385\n", - " 139.887\n", - " 140.389\n", - " 140.891\n", - " 141.393\n", - " 141.895\n", - " 142.398\n", - " 142.900\n", - " 143.402\n", - " 143.904\n", - " 144.406\n", - " 144.909\n", - " 145.411\n", - " 145.913\n", - " 146.415\n", - " 146.917\n", - " 147.419\n", - " 147.922\n", - " 148.424\n", - " 148.926\n", - " 149.429\n", - " 149.930\n", - " 150.433\n", - " 150.935\n", - " 151.437\n", - " 151.939\n", - " 152.441\n", - " 152.944\n", - " 153.446\n", - " 153.948\n", - " 154.450\n", - " 154.952\n", - " 155.455\n", - " 155.957\n", - " 156.459\n", - " 156.961\n", - " 157.464\n", - " 157.966\n", - " 158.468\n", - " 158.970\n", - " 159.472\n", - " 159.975\n", - " 160.477\n", - " 160.979\n", - " 161.481\n", - " 161.983\n", - " 162.486\n", - " 162.988\n", - " 163.490\n", - " 163.992\n", - " 164.494\n", - " 164.996\n", - " 165.499\n", - " 166.001\n", - " 166.503\n", - " 167.005\n", - " 167.507\n", - " 168.010\n", - " 168.512\n", - " 169.014\n", - " 169.516\n", - " 170.018\n", - " 170.521\n", - " 171.023\n", - " 171.525\n", - " 172.027\n", - " 172.529\n", - " 173.032\n", - " 173.534\n", - " 174.036\n", - " 174.538\n", - " 175.040\n", - " 175.543\n", - " 176.045\n", - " 176.547\n", - " 177.049\n", - " 177.551\n", - " 178.054\n", - " 178.556\n", - " 179.058\n", - " 179.560\n", - " 180.062\n", - " 180.564\n", - " 181.067\n", - " 181.569\n", - " 182.071\n", - " 182.573\n", - " 183.075\n", - " 183.578\n", - " 184.080\n", - " 184.582\n", - " 185.084\n", - " 185.586\n", - " 186.089\n", - " 186.591\n", - " 187.093\n", - " 187.595\n", - " 188.097\n", - " 188.600\n", - " 189.102\n", - " 189.604\n", - " 190.106\n", - " 190.608\n", - " 191.111\n", - " 191.613\n", - " 192.115\n", - " 192.617\n", - " 193.119\n", - " 193.621\n", - " 194.124\n", - " 194.626\n", - " 195.128\n", - " 195.630\n", - " 196.132\n", - " 196.635\n", - " 197.137\n", - " 197.639\n", - " 198.141\n", - " 198.643\n", - " 199.146\n", - " 199.648\n", - " 200.150\n", - " 200.652\n", - " 201.154\n", - " 201.657\n", - " 202.159\n", - " 202.661\n", - " 203.163\n", - " 203.665\n", - " 204.168\n", - " 204.670\n", - " 205.172\n", - " 205.674\n", - " 206.176\n", - " 206.678\n", - " 207.181\n", - " 207.683\n", - " 208.185\n", - " 208.687\n", - " 209.190\n", - " 209.692\n", - " 210.194\n", - " 210.696\n", - " 211.198\n", - " 211.701\n", - " 212.203\n", - " 212.705\n", - " 213.207\n", - " 213.709\n", - " 214.212\n", - " 214.714\n", - " 215.216\n", - " 215.718\n", - " 216.220\n", - " 216.722\n", - " 217.225\n", - " 217.727\n", - " 218.229\n", - " 218.731\n", - " 219.233\n", - " 219.736\n", - " 220.238\n", - " 220.740\n", - " 221.242\n", - " 221.744\n", - " 222.246\n", - " 222.749\n", - " 223.251\n", - " 223.753\n", - " 224.256\n", - " 224.757\n", - " 225.260\n", - " 225.762\n", - " 226.264\n", - " 226.767\n", - " 227.268\n", - " 227.771\n", - " 228.273\n", - "\n", - " duration =\n", - "\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - " 0.50000\n", - "\n", - " trial_type =\n", - "\n", - " 5\n", - " 8\n", - " 2\n", - " 8\n", - " 7\n", - " 8\n", - " 5\n", - " 8\n", - " 2\n", - " 8\n", - " 4\n", - " 8\n", - " 2\n", - " 8\n", - " 1\n", - " 8\n", - " 2\n", - " 8\n", - " 1\n", - " 8\n", - " 3\n", - " 8\n", - " 4\n", - " 8\n", - " 6\n", - " 8\n", - " 3\n", - " 8\n", - " 3\n", - " 8\n", - " 4\n", - " 8\n", - " 5\n", - " 8\n", - " 6\n", - " 8\n", - " 6\n", - " 8\n", - " 4\n", - " 8\n", - " 7\n", - " 8\n", - " 2\n", - " 8\n", - " 7\n", - " 8\n", - " 5\n", - " 8\n", - " 1\n", - " 8\n", - " 6\n", - " 8\n", - " 3\n", - " 8\n", - " 5\n", - " 8\n", - " 5\n", - " 8\n", - " 2\n", - " 8\n", - " 3\n", - " 8\n", - " 5\n", - " 8\n", - " 7\n", - " 8\n", - " 1\n", - " 8\n", - " 6\n", - " 8\n", - " 1\n", - " 8\n", - " 5\n", - " 8\n", - " 1\n", - " 8\n", - " 6\n", - " 8\n", - " 6\n", - " 8\n", - " 6\n", - " 8\n", - " 2\n", - " 8\n", - " 6\n", - " 8\n", - " 7\n", - " 8\n", - " 7\n", - " 8\n", - " 2\n", - " 8\n", - " 7\n", - " 8\n", - " 3\n", - " 8\n", - " 7\n", - " 8\n", - " 6\n", - " 8\n", - " 5\n", - " 8\n", - " 3\n", - " 8\n", - " 3\n", - " 8\n", - " 7\n", - " 8\n", - " 2\n", - " 8\n", - " 1\n", - " 8\n", - " 6\n", - " 8\n", - " 3\n", - " 8\n", - " 1\n", - " 8\n", - " 3\n", - " 8\n", - " 1\n", - " 8\n", - " 3\n", - " 8\n", - " 4\n", - " 8\n", - " 7\n", - " 8\n", - " 5\n", - " 8\n", - " 4\n", - " 8\n", - " 2\n", - " 8\n", - " 2\n", - " 8\n", - " 5\n", - " 8\n", - " 2\n", - " 8\n", - " 6\n", - " 8\n", - " 1\n", - " 8\n", - " 5\n", - " 8\n", - " 7\n", - " 8\n", - " 4\n", - " 8\n", - " 6\n", - " 8\n", - " 5\n", - " 8\n", - " 6\n", - " 8\n", - " 3\n", - " 8\n", - " 6\n", - " 8\n", - " 4\n", - " 8\n", - " 4\n", - " 8\n", - " 1\n", - " 8\n", - " 5\n", - " 8\n", - " 4\n", - " 8\n", - " 2\n", - " 8\n", - " 3\n", - " 8\n", - " 2\n", - " 8\n", - " 3\n", - " 8\n", - " 5\n", - " 8\n", - " 6\n", - " 8\n", - " 2\n", - " 8\n", - " 2\n", - " 8\n", - " 1\n", - " 8\n", - " 7\n", - " 8\n", - " 5\n", - " 8\n", - " 7\n", - " 8\n", - " 7\n", - " 8\n", - " 4\n", - " 8\n", - " 7\n", - " 8\n", - " 1\n", - " 8\n", - " 1\n", - " 8\n", - " 2\n", - " 8\n", - " 4\n", - " 8\n", - " 4\n", - " 8\n", - " 4\n", - " 8\n", - " 6\n", - " 8\n", - " 1\n", - " 8\n", - " 2\n", - " 8\n", - " 4\n", - " 8\n", - " 1\n", - " 8\n", - " 4\n", - " 8\n", - " 4\n", - " 8\n", - " 2\n", - " 8\n", - " 2\n", - " 8\n", - " 2\n", - " 8\n", - " 7\n", - " 8\n", - " 1\n", - " 8\n", - " 1\n", - " 8\n", - " 1\n", - " 8\n", - " 4\n", - " 8\n", - " 3\n", - " 8\n", - " 5\n", - " 8\n", - " 3\n", - " 8\n", - " 3\n", - " 8\n", - " 7\n", - " 8\n", - " 6\n", - " 8\n", - " 6\n", - " 8\n", - " 1\n", - " 8\n", - " 7\n", - " 8\n", - " 6\n", - " 8\n", - " 6\n", - " 8\n", - " 5\n", - " 8\n", - " 3\n", - " 8\n", - " 4\n", - " 8\n", - " 3\n", - " 8\n", - " 3\n", - " 8\n", - " 3\n", - " 8\n", - " 5\n", - " 8\n", - " 4\n", - " 8\n", - " 7\n", - " 8\n", - " 5\n", - " 8\n", - " 5\n", - " 8\n", - " 5\n", - " 8\n", - " 4\n", - " 8\n", - " 2\n", - " 8\n", - " 6\n", - " 8\n", - " 2\n", - " 8\n", - " 1\n", - " 8\n", - " 7\n", - " 8\n", - " 7\n", - " 8\n", - " 5\n", - " 8\n", - " 3\n", - " 8\n", - " 5\n", - " 8\n", - " 6\n", - " 8\n", - " 1\n", - " 8\n", - " 5\n", - " 8\n", - " 2\n", - " 8\n", - " 6\n", - " 8\n", - " 7\n", - " 8\n", - " 5\n", - " 8\n", - " 7\n", - " 8\n", - " 4\n", - " 8\n", - " 4\n", - " 8\n", - " 7\n", - " 8\n", - " 3\n", - " 8\n", - " 7\n", - " 8\n", - " 2\n", - " 8\n", - " 1\n", - " 8\n", - " 3\n", - " 8\n", - " 1\n", - " 8\n", - " 1\n", - " 8\n", - " 5\n", - " 8\n", - " 2\n", - " 8\n", - " 3\n", - " 8\n", - " 7\n", - " 8\n", - " 6\n", - " 8\n", - " 1\n", - " 8\n", - " 6\n", - " 8\n", - " 7\n", - " 8\n", - " 2\n", - " 8\n", - " 3\n", - " 8\n", - " 4\n", - " 8\n", - " 4\n", - " 8\n", - " 1\n", - " 8\n", - " 7\n", - " 8\n", - " 3\n", - " 8\n", - " 5\n", - " 8\n", - " 6\n", - " 8\n", - " 2\n", - " 8\n", - " 4\n", - " 8\n", - " 3\n", - " 8\n", - " 5\n", - " 8\n", - " 2\n", - " 8\n", - " 6\n", - " 8\n", - " 4\n", - " 8\n", - " 7\n", - " 8\n", - " 3\n", - " 8\n", - " 1\n", - " 8\n", - " 4\n", - " 8\n", - " 1\n", - " 8\n", - " 3\n", - " 8\n", - " 4\n", - " 8\n", - " 6\n", - " 8\n", - " 2\n", - " 8\n", - " 6\n", - " 8\n", - " 4\n", - " 8\n", - " 1\n", - " 8\n", - " 5\n", - " 8\n", - " 7\n", - " 8\n", - "\n", - " stim_file = \n", - " {\n", - " [1,1] = stim_129.png\n", - " [2,1] = stim_211.png\n", - " [3,1] = stim_36.png\n", - " [4,1] = stim_211.png\n", - " [5,1] = stim_181.png\n", - " [6,1] = stim_211.png\n", - " [7,1] = stim_145.png\n", - " [8,1] = stim_211.png\n", - " [9,1] = stim_53.png\n", - " [10,1] = stim_211.png\n", - " [11,1] = stim_104.png\n", - " [12,1] = stim_211.png\n", - " [13,1] = stim_32.png\n", - " [14,1] = stim_211.png\n", - " [15,1] = stim_5.png\n", - " [16,1] = stim_211.png\n", - " [17,1] = stim_37.png\n", - " [18,1] = stim_211.png\n", - " [19,1] = stim_23.png\n", - " [20,1] = stim_211.png\n", - " [21,1] = stim_64.png\n", - " [22,1] = stim_211.png\n", - " [23,1] = stim_93.png\n", - " [24,1] = stim_211.png\n", - " [25,1] = stim_163.png\n", - " [26,1] = stim_211.png\n", - " [27,1] = stim_82.png\n", - " [28,1] = stim_211.png\n", - " [29,1] = stim_77.png\n", - " [30,1] = stim_211.png\n", - " [31,1] = stim_112.png\n", - " [32,1] = stim_211.png\n", - " [33,1] = stim_121.png\n", - " [34,1] = stim_211.png\n", - " [35,1] = stim_165.png\n", - " [36,1] = stim_211.png\n", - " [37,1] = stim_161.png\n", - " [38,1] = stim_211.png\n", - " [39,1] = stim_114.png\n", - " [40,1] = stim_211.png\n", - " [41,1] = stim_195.png\n", - " [42,1] = stim_211.png\n", - " [43,1] = stim_47.png\n", - " [44,1] = stim_211.png\n", - " [45,1] = stim_208.png\n", - " [46,1] = stim_211.png\n", - " [47,1] = stim_123.png\n", - " [48,1] = stim_211.png\n", - " [49,1] = stim_8.png\n", - " [50,1] = stim_211.png\n", - " [51,1] = stim_155.png\n", - " [52,1] = stim_211.png\n", - " [53,1] = stim_81.png\n", - " [54,1] = stim_211.png\n", - " [55,1] = stim_146.png\n", - " [56,1] = stim_211.png\n", - " [57,1] = stim_131.png\n", - " [58,1] = stim_211.png\n", - " [59,1] = stim_50.png\n", - " [60,1] = stim_211.png\n", - " [61,1] = stim_65.png\n", - " [62,1] = stim_211.png\n", - " [63,1] = stim_136.png\n", - " [64,1] = stim_211.png\n", - " [65,1] = stim_189.png\n", - " [66,1] = stim_211.png\n", - " [67,1] = stim_27.png\n", - " [68,1] = stim_211.png\n", - " [69,1] = stim_177.png\n", - " [70,1] = stim_211.png\n", - " [71,1] = stim_21.png\n", - " [72,1] = stim_211.png\n", - " [73,1] = stim_137.png\n", - " [74,1] = stim_211.png\n", - " [75,1] = stim_30.png\n", - " [76,1] = stim_211.png\n", - " [77,1] = stim_171.png\n", - " [78,1] = stim_211.png\n", - " [79,1] = stim_151.png\n", - " [80,1] = stim_211.png\n", - " [81,1] = stim_169.png\n", - " [82,1] = stim_211.png\n", - " [83,1] = stim_58.png\n", - " [84,1] = stim_211.png\n", - " [85,1] = stim_174.png\n", - " [86,1] = stim_211.png\n", - " [87,1] = stim_197.png\n", - " [88,1] = stim_211.png\n", - " [89,1] = stim_204.png\n", - " [90,1] = stim_211.png\n", - " [91,1] = stim_43.png\n", - " [92,1] = stim_211.png\n", - " [93,1] = stim_185.png\n", - " [94,1] = stim_211.png\n", - " [95,1] = stim_80.png\n", - " [96,1] = stim_211.png\n", - " [97,1] = stim_188.png\n", - " [98,1] = stim_211.png\n", - " [99,1] = stim_159.png\n", - " [100,1] = stim_211.png\n", - " [101,1] = stim_128.png\n", - " [102,1] = stim_211.png\n", - " [103,1] = stim_66.png\n", - " [104,1] = stim_211.png\n", - " [105,1] = stim_79.png\n", - " [106,1] = stim_211.png\n", - " [107,1] = stim_209.png\n", - " [108,1] = stim_211.png\n", - " [109,1] = stim_39.png\n", - " [110,1] = stim_211.png\n", - " [111,1] = stim_12.png\n", - " [112,1] = stim_211.png\n", - " [113,1] = stim_152.png\n", - " [114,1] = stim_211.png\n", - " [115,1] = stim_75.png\n", - " [116,1] = stim_211.png\n", - " [117,1] = stim_28.png\n", - " [118,1] = stim_211.png\n", - " [119,1] = stim_83.png\n", - " [120,1] = stim_211.png\n", - " [121,1] = stim_3.png\n", - " [122,1] = stim_211.png\n", - " [123,1] = stim_72.png\n", - " [124,1] = stim_211.png\n", - " [125,1] = stim_108.png\n", - " [126,1] = stim_211.png\n", - " [127,1] = stim_182.png\n", - " [128,1] = stim_211.png\n", - " [129,1] = stim_144.png\n", - " [130,1] = stim_211.png\n", - " [131,1] = stim_119.png\n", - " [132,1] = stim_211.png\n", - " [133,1] = stim_55.png\n", - " [134,1] = stim_211.png\n", - " [135,1] = stim_41.png\n", - " [136,1] = stim_211.png\n", - " [137,1] = stim_135.png\n", - " [138,1] = stim_211.png\n", - " [139,1] = stim_51.png\n", - " [140,1] = stim_211.png\n", - " [141,1] = stim_170.png\n", - " [142,1] = stim_211.png\n", - " [143,1] = stim_13.png\n", - " [144,1] = stim_211.png\n", - " [145,1] = stim_133.png\n", - " [146,1] = stim_211.png\n", - " [147,1] = stim_210.png\n", - " [148,1] = stim_211.png\n", - " [149,1] = stim_113.png\n", - " [150,1] = stim_211.png\n", - " [151,1] = stim_166.png\n", - " [152,1] = stim_211.png\n", - " [153,1] = stim_149.png\n", - " [154,1] = stim_211.png\n", - " [155,1] = stim_175.png\n", - " [156,1] = stim_211.png\n", - " [157,1] = stim_63.png\n", - " [158,1] = stim_211.png\n", - " [159,1] = stim_158.png\n", - " [160,1] = stim_211.png\n", - " [161,1] = stim_116.png\n", - " [162,1] = stim_211.png\n", - " [163,1] = stim_106.png\n", - " [164,1] = stim_211.png\n", - " [165,1] = stim_17.png\n", - " [166,1] = stim_211.png\n", - " [167,1] = stim_143.png\n", - " [168,1] = stim_211.png\n", - " [169,1] = stim_110.png\n", - " [170,1] = stim_211.png\n", - " [171,1] = stim_56.png\n", - " [172,1] = stim_211.png\n", - " [173,1] = stim_84.png\n", - " [174,1] = stim_211.png\n", - " [175,1] = stim_34.png\n", - " [176,1] = stim_211.png\n", - " [177,1] = stim_62.png\n", - " [178,1] = stim_211.png\n", - " [179,1] = stim_127.png\n", - " [180,1] = stim_211.png\n", - " [181,1] = stim_173.png\n", - " [182,1] = stim_211.png\n", - " [183,1] = stim_60.png\n", - " [184,1] = stim_211.png\n", - " [185,1] = stim_38.png\n", - " [186,1] = stim_211.png\n", - " [187,1] = stim_14.png\n", - " [188,1] = stim_211.png\n", - " [189,1] = stim_193.png\n", - " [190,1] = stim_211.png\n", - " [191,1] = stim_139.png\n", - " [192,1] = stim_211.png\n", - " [193,1] = stim_198.png\n", - " [194,1] = stim_211.png\n", - " [195,1] = stim_192.png\n", - " [196,1] = stim_211.png\n", - " [197,1] = stim_99.png\n", - " [198,1] = stim_211.png\n", - " [199,1] = stim_187.png\n", - " [200,1] = stim_211.png\n", - " [201,1] = stim_29.png\n", - " [202,1] = stim_211.png\n", - " [203,1] = stim_20.png\n", - " [204,1] = stim_211.png\n", - " [205,1] = stim_57.png\n", - " [206,1] = stim_211.png\n", - " [207,1] = stim_117.png\n", - " [208,1] = stim_211.png\n", - " [209,1] = stim_95.png\n", - " [210,1] = stim_211.png\n", - " [211,1] = stim_111.png\n", - " [212,1] = stim_211.png\n", - " [213,1] = stim_172.png\n", - " [214,1] = stim_211.png\n", - " [215,1] = stim_2.png\n", - " [216,1] = stim_211.png\n", - " [217,1] = stim_42.png\n", - " [218,1] = stim_211.png\n", - " [219,1] = stim_109.png\n", - " [220,1] = stim_211.png\n", - " [221,1] = stim_7.png\n", - " [222,1] = stim_211.png\n", - " [223,1] = stim_100.png\n", - " [224,1] = stim_211.png\n", - " [225,1] = stim_115.png\n", - " [226,1] = stim_211.png\n", - " [227,1] = stim_59.png\n", - " [228,1] = stim_211.png\n", - " [229,1] = stim_48.png\n", - " [230,1] = stim_211.png\n", - " [231,1] = stim_31.png\n", - " [232,1] = stim_211.png\n", - " [233,1] = stim_190.png\n", - " [234,1] = stim_211.png\n", - " [235,1] = stim_6.png\n", - " [236,1] = stim_211.png\n", - " [237,1] = stim_10.png\n", - " [238,1] = stim_211.png\n", - " [239,1] = stim_24.png\n", - " [240,1] = stim_211.png\n", - " [241,1] = stim_103.png\n", - " [242,1] = stim_211.png\n", - " [243,1] = stim_85.png\n", - " [244,1] = stim_211.png\n", - " [245,1] = stim_130.png\n", - " [246,1] = stim_211.png\n", - " [247,1] = stim_70.png\n", - " [248,1] = stim_211.png\n", - " [249,1] = stim_78.png\n", - " [250,1] = stim_211.png\n", - " [251,1] = stim_191.png\n", - " [252,1] = stim_211.png\n", - " [253,1] = stim_160.png\n", - " [254,1] = stim_211.png\n", - " [255,1] = stim_157.png\n", - " [256,1] = stim_211.png\n", - " [257,1] = stim_22.png\n", - " [258,1] = stim_211.png\n", - " [259,1] = stim_196.png\n", - " [260,1] = stim_211.png\n", - " [261,1] = stim_153.png\n", - " [262,1] = stim_211.png\n", - " [263,1] = stim_154.png\n", - " [264,1] = stim_211.png\n", - " [265,1] = stim_126.png\n", - " [266,1] = stim_211.png\n", - " [267,1] = stim_90.png\n", - " [268,1] = stim_211.png\n", - " [269,1] = stim_102.png\n", - " [270,1] = stim_211.png\n", - " [271,1] = stim_86.png\n", - " [272,1] = stim_211.png\n", - " [273,1] = stim_67.png\n", - " [274,1] = stim_211.png\n", - " [275,1] = stim_71.png\n", - " [276,1] = stim_211.png\n", - " [277,1] = stim_147.png\n", - " [278,1] = stim_211.png\n", - " [279,1] = stim_97.png\n", - " [280,1] = stim_211.png\n", - " [281,1] = stim_207.png\n", - " [282,1] = stim_211.png\n", - " [283,1] = stim_142.png\n", - " [284,1] = stim_211.png\n", - " [285,1] = stim_125.png\n", - " [286,1] = stim_211.png\n", - " [287,1] = stim_150.png\n", - " [288,1] = stim_211.png\n", - " [289,1] = stim_107.png\n", - " [290,1] = stim_211.png\n", - " [291,1] = stim_44.png\n", - " [292,1] = stim_211.png\n", - " [293,1] = stim_168.png\n", - " [294,1] = stim_211.png\n", - " [295,1] = stim_45.png\n", - " [296,1] = stim_211.png\n", - " [297,1] = stim_15.png\n", - " [298,1] = stim_211.png\n", - " [299,1] = stim_184.png\n", - " [300,1] = stim_211.png\n", - " [301,1] = stim_202.png\n", - " [302,1] = stim_211.png\n", - " [303,1] = stim_138.png\n", - " [304,1] = stim_211.png\n", - " [305,1] = stim_76.png\n", - " [306,1] = stim_211.png\n", - " [307,1] = stim_140.png\n", - " [308,1] = stim_211.png\n", - " [309,1] = stim_180.png\n", - " [310,1] = stim_211.png\n", - " [311,1] = stim_25.png\n", - " [312,1] = stim_211.png\n", - " [313,1] = stim_132.png\n", - " [314,1] = stim_211.png\n", - " [315,1] = stim_52.png\n", - " [316,1] = stim_211.png\n", - " [317,1] = stim_164.png\n", - " [318,1] = stim_211.png\n", - " [319,1] = stim_199.png\n", - " [320,1] = stim_211.png\n", - " [321,1] = stim_141.png\n", - " [322,1] = stim_211.png\n", - " [323,1] = stim_203.png\n", - " [324,1] = stim_211.png\n", - " [325,1] = stim_91.png\n", - " [326,1] = stim_211.png\n", - " [327,1] = stim_120.png\n", - " [328,1] = stim_211.png\n", - " [329,1] = stim_186.png\n", - " [330,1] = stim_211.png\n", - " [331,1] = stim_61.png\n", - " [332,1] = stim_211.png\n", - " [333,1] = stim_205.png\n", - " [334,1] = stim_211.png\n", - " [335,1] = stim_33.png\n", - " [336,1] = stim_211.png\n", - " [337,1] = stim_1.png\n", - " [338,1] = stim_211.png\n", - " [339,1] = stim_88.png\n", - " [340,1] = stim_211.png\n", - " [341,1] = stim_9.png\n", - " [342,1] = stim_211.png\n", - " [343,1] = stim_26.png\n", - " [344,1] = stim_211.png\n", - " [345,1] = stim_148.png\n", - " [346,1] = stim_211.png\n", - " [347,1] = stim_46.png\n", - " [348,1] = stim_211.png\n", - " [349,1] = stim_87.png\n", - " [350,1] = stim_211.png\n", - " [351,1] = stim_201.png\n", - " [352,1] = stim_211.png\n", - " [353,1] = stim_176.png\n", - " [354,1] = stim_211.png\n", - " [355,1] = stim_16.png\n", - " [356,1] = stim_211.png\n", - " [357,1] = stim_162.png\n", - " [358,1] = stim_211.png\n", - " [359,1] = stim_194.png\n", - " [360,1] = stim_211.png\n", - " [361,1] = stim_49.png\n", - " [362,1] = stim_211.png\n", - " [363,1] = stim_69.png\n", - " [364,1] = stim_211.png\n", - " [365,1] = stim_92.png\n", - " [366,1] = stim_211.png\n", - " [367,1] = stim_101.png\n", - " [368,1] = stim_211.png\n", - " [369,1] = stim_19.png\n", - " [370,1] = stim_211.png\n", - " [371,1] = stim_206.png\n", - " [372,1] = stim_211.png\n", - " [373,1] = stim_74.png\n", - " [374,1] = stim_211.png\n", - " [375,1] = stim_124.png\n", - " [376,1] = stim_211.png\n", - " [377,1] = stim_178.png\n", - " [378,1] = stim_211.png\n", - " [379,1] = stim_40.png\n", - " [380,1] = stim_211.png\n", - " [381,1] = stim_94.png\n", - " [382,1] = stim_211.png\n", - " [383,1] = stim_89.png\n", - " [384,1] = stim_211.png\n", - " [385,1] = stim_122.png\n", - " [386,1] = stim_211.png\n", - " [387,1] = stim_35.png\n", - " [388,1] = stim_211.png\n", - " [389,1] = stim_179.png\n", - " [390,1] = stim_211.png\n", - " [391,1] = stim_98.png\n", - " [392,1] = stim_211.png\n", - " [393,1] = stim_200.png\n", - " [394,1] = stim_211.png\n", - " [395,1] = stim_68.png\n", - " [396,1] = stim_211.png\n", - " [397,1] = stim_18.png\n", - " [398,1] = stim_211.png\n", - " [399,1] = stim_105.png\n", - " [400,1] = stim_211.png\n", - " [401,1] = stim_4.png\n", - " [402,1] = stim_211.png\n", - " [403,1] = stim_73.png\n", - " [404,1] = stim_211.png\n", - " [405,1] = stim_96.png\n", - " [406,1] = stim_211.png\n", - " [407,1] = stim_167.png\n", - " [408,1] = stim_211.png\n", - " [409,1] = stim_54.png\n", - " [410,1] = stim_211.png\n", - " [411,1] = stim_156.png\n", - " [412,1] = stim_211.png\n", - " [413,1] = stim_118.png\n", - " [414,1] = stim_211.png\n", - " [415,1] = stim_11.png\n", - " [416,1] = stim_211.png\n", - " [417,1] = stim_134.png\n", - " [418,1] = stim_211.png\n", - " [419,1] = stim_183.png\n", - " [420,1] = stim_211.png\n", - " }\n", - "\n" - ] - } - ], - "source": [ - "bids.util.tsvread(events_file{1})" - ] - }, - { - "cell_type": "markdown", - "id": "ebe684a3", - "metadata": {}, - "source": [ - "## Write to TSV files" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "e36c8029", - "metadata": {}, - "outputs": [], - "source": [ - "tsv_file = fullfile(pwd, 'output', 'sub-01_task-STRUCTURE_events.tsv');\n", - "\n", - "logFile.onset = [2; NaN];\n", - "logFile.trial_type = {'motion_up'; 'static'};\n", - "logFile.duration = [1; 4];\n", - "logFile.speed = [NaN; 4];\n", - "logFile.is_fixation = {'true'; '3'};\n", - "\n", - "bids.util.tsvwrite(tsv_file, logFile);" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "a5214322", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "onset\ttrial_type\tduration\tspeed\tis_fixation\n", - "2\tmotion_up\t1\tn/a\ttrue\n", - "n/a\tstatic\t4\t4\t3\n", - "\n" - ] - } - ], - "source": [ - "!cat output/sub-01_task-STRUCTURE_events.tsv" - ] - }, - { - "cell_type": "markdown", - "id": "792e2f13", - "metadata": {}, - "source": [ - "## Write to JSON files" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "abc73827", - "metadata": {}, - "outputs": [], - "source": [ - "content = struct( 'Name', 'test', ...\n", - " 'BIDSVersion', '1.6', ...\n", - " 'DatasetType', 'raw', ...\n", - " 'License', '', ...\n", - " 'Acknowledgements', '', ...\n", - " 'HowToAcknowledge', '', ...\n", - " 'DatasetDOI', '', ...\n", - " 'HEDVersion', '', ...\n", - " 'Funding', {{}}, ...\n", - " 'Authors', {{}}, ...\n", - " 'ReferencesAndLinks', {{}});\n", - "\n", - "bids.util.jsonencode(fullfile(pwd, 'output', 'dataset_description.json'), content)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "daae43aa", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\n", - " \"Name\": \"test\",\n", - " \"BIDSVersion\": \"1.6\",\n", - " \"DatasetType\": \"raw\",\n", - " \"License\": \"\",\n", - " \"Acknowledgements\": \"\",\n", - " \"HowToAcknowledge\": \"\",\n", - " \"DatasetDOI\": \"\",\n", - " \"HEDVersion\": \"\",\n", - " \"Funding\": [],\n", - " \"Authors\": [],\n", - " \"ReferencesAndLinks\": []\n", - "}\n" - ] - } - ], - "source": [ - "!cat output/dataset_description.json" - ] - }, - { - "cell_type": "markdown", - "id": "ea84dfb0", - "metadata": {}, - "source": [ - "## Read from JSON files" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "ae689e2a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ans =\n", - "\n", - " scalar structure containing the fields:\n", - "\n", - " Name = test\n", - " BIDSVersion = 1.6\n", - " DatasetType = raw\n", - " License = \n", - " Acknowledgements = \n", - " HowToAcknowledge = \n", - " DatasetDOI = \n", - " HEDVersion = \n", - " Funding = [](0x0)\n", - " Authors = [](0x0)\n", - " ReferencesAndLinks = [](0x0)\n", - "\n" - ] - } - ], - "source": [ - "bids.util.jsondecode(fullfile(pwd, 'output', 'dataset_description.json'))" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Octave", - "language": "octave", - "name": "octave" - }, - "language_info": { - "file_extension": ".m", - "help_links": [ - { - "text": "GNU Octave", - "url": "https://www.gnu.org/software/octave/support.html" - }, - { - "text": "Octave Kernel", - "url": "https://github.com/Calysto/octave_kernel" - }, - { - "text": "MetaKernel Magics", - "url": "https://metakernel.readthedocs.io/en/latest/source/README.html" - } - ], - "mimetype": "text/x-octave", - "name": "octave", - "version": "4.2.2" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/miss_hit.cfg b/miss_hit.cfg index ec7c2ce8..34947989 100644 --- a/miss_hit.cfg +++ b/miss_hit.cfg @@ -2,17 +2,17 @@ project_root -exclude_dir: "examples" - copyright_entity: "Guillaume Flandin, Wellcome Centre for Human Neuroimaging" copyright_entity: "Remi Gau" copyright_entity: "Agah Karakuzu" copyright_entity: "BIDS-MATLAB developers" -regex_class_name: "([A-Z]){1}([a-z]+)*" -regex_method_name: "[a-z]+(_[a-z]+)*" +regex_class_name: "(([A-Z]){1}([a-z]+)*)*" +regex_method_name: "[a-zA-Z]+(_[a-z]+)*" regex_parameter_name: "[A-Z]*|[a-z]+(_[a-z]+)*" regex_function_name: "[a-z]+(_[a-zA-Z0-9]+)*" +regex_nested_name: "[a-z]+(_[a-zA-Z0-9]+)*" +regex_script_name: "[a-zA-Z]+(_[a-zA-Z0-9]+)*" line_length: 100 @@ -20,6 +20,7 @@ tab_width: 2 # metrics limit for the code quality (https://florianschanda.github.io/miss_hit/metrics.html) metric "cnest": limit 5 -metric "file_length": limit 750 -metric "cyc": limit 17 +metric "file_length": limit 1500 +suppress_rule: "file_length" +metric "cyc": limit 21 metric "parameters": limit 8 diff --git a/requirements.txt b/requirements.txt index 08da75b5..9445f2e7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -miss_hit==0.9.34 +miss_hit==0.9.35 pre-commit jupyterlab octave_kernel diff --git a/run_tests.m b/run_tests.m index 23d972b0..9c00e6e2 100644 --- a/run_tests.m +++ b/run_tests.m @@ -1,8 +1,13 @@ -function success = run_tests() +function success = run_tests(with_coverage) % + % (C) Copyright 2021 BIDS-MATLAB developers - with_coverage = false; + fprintf('\nRunning tests\n'); + + if nargin < 1 + with_coverage = true; + end addpath(fullfile(pwd, 'tests', 'utils')); diff --git a/templates/README b/templates/README.template similarity index 100% rename from templates/README rename to templates/README.template diff --git a/templates/README_datalad b/templates/README_datalad.template similarity index 94% rename from templates/README_datalad rename to templates/README_datalad.template index 21772a19..e05940e7 100644 --- a/templates/README_datalad +++ b/templates/README_datalad.template @@ -151,7 +151,8 @@ Some examples: Any additional information or pointers to information that might be helpful to users of the dataset. Include qualitative information related to how the data acquisition went. -________________________________________________________________________________ + +--- [![made-with-datalad](https://www.datalad.org/badges/made_with.svg)](https://datalad.org) @@ -177,7 +178,7 @@ datalad clone Once a dataset is cloned, it is a light-weight directory on your local machine. At this point, it contains only small metadata and information on the -identity of the files in the dataset, but not actual *content* of the +identity of the files in the dataset, but not actual _content_ of the (sometimes large) data files. ### Retrieve dataset content @@ -191,7 +192,7 @@ datalad get ` This command will trigger a download of the files, directories, or subdatasets you have specified. -DataLad datasets can contain other datasets, so called *subdatasets*. +DataLad datasets can contain other datasets, so called _subdatasets_. If you clone the top-level dataset, subdatasets do not yet contain metadata and information on the identity of files, but appear to be empty directories. In order to retrieve file availability metadata in @@ -209,19 +210,19 @@ subdataset will be downloaded at once. ### Stay up-to-date DataLad datasets can be updated. The command `datalad update` will -*fetch* updates and store them on a different branch (by default +_fetch_ updates and store them on a different branch (by default `remotes/origin/master`). Running ``` datalad update --merge ``` -will *pull* available updates and integrate them in one go. +will _pull_ available updates and integrate them in one go. ### Find out what has been done -DataLad datasets contain their history in the ``git log``. -By running ``git log`` (or a tool that displays Git history) in the dataset or on +DataLad datasets contain their history in the `git log`. +By running `git log` (or a tool that displays Git history) in the dataset or on specific files, you can find out what has been done to the dataset or to individual files by whom, and when. diff --git a/templates/boilerplates/anat.tmp b/templates/boilerplates/anat.tmp index c6b030f3..e17b4435 100644 --- a/templates/boilerplates/anat.tmp +++ b/templates/boilerplates/anat.tmp @@ -1,4 +1,4 @@ {{PulseSequenceType}} {{ScanningSequence}} {{SequenceVariant}} {{suffix}} structural MRI data were collected ({{nb_slices}} slices; repetition time, TR= {{RepetitionTime}} s; -echo time, TE= {{echo_time}} ms; flip angle, FA= {{FlipAngle}} deg; field of view, FOV= {{fov}} mm; +echo time, TE= {{echo_time}} s; flip angle, FA= {{FlipAngle}} deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}}; voxel size= {{vox_size}} mm). diff --git a/templates/boilerplates/dwi.tmp b/templates/boilerplates/dwi.tmp index 32efde19..f6534800 100644 --- a/templates/boilerplates/dwi.tmp +++ b/templates/boilerplates/dwi.tmp @@ -1 +1 @@ -{{nb_runs}} run of {{PulseSequenceType}} {{ScanningSequence}} {{SequenceVariant}} diffusion-weighted-imaging ({{suffix}}) data were collected ({{nb_slices}} slices ; repetition time, TR= {{RepetitionTime}} s; echo time, TE= {{echo_time}} ms; flip angle, FA= {{FlipAngle}} deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}} ; voxel size= {{vox_size}} mm ; b-values of XXX acquired; XXX diffusion directions; multiband factor= {{mb_str}} ). +{{nb_runs}} run of {{PulseSequenceType}} {{ScanningSequence}} {{SequenceVariant}} diffusion-weighted-imaging ({{suffix}}) data were collected ({{nb_slices}} slices ; repetition time, TR= {{RepetitionTime}} s; echo time, TE= {{echo_time}} s; flip angle, FA= {{FlipAngle}} deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}} ; voxel size= {{vox_size}} mm ; b-values of XXX acquired; XXX diffusion directions; multiband factor= {{mb_str}} ). diff --git a/templates/boilerplates/fmap.tmp b/templates/boilerplates/fmap.tmp index e9b9534a..6aecd509 100644 --- a/templates/boilerplates/fmap.tmp +++ b/templates/boilerplates/fmap.tmp @@ -1 +1 @@ -A {{PulseSequenceType}} {{ScanningSequence}} {{SequenceVariant}} field map (phase encoding: {{PhaseEncodingDirection}}; {{nb_slices}} slices; repetition time, TR= {{RepetitionTime}} s; echo time 1 / 2, TE 1/2= {{echo_time}} ms; flip angle, FA= {{FlipAngle}} deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}}; voxel size= {{vox_size}} mm) was acquired. +A {{PulseSequenceType}} {{ScanningSequence}} {{SequenceVariant}} field map (phase encoding: {{PhaseEncodingDirection}}; {{nb_slices}} slices; repetition time, TR= {{RepetitionTime}} s; echo time 1 / 2, TE 1/2= {{echo_time}} s; flip angle, FA= {{FlipAngle}} deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}}; voxel size= {{vox_size}} mm) was acquired. diff --git a/templates/boilerplates/func.tmp b/templates/boilerplates/func.tmp index 543fe414..07c78515 100644 --- a/templates/boilerplates/func.tmp +++ b/templates/boilerplates/func.tmp @@ -1,5 +1,5 @@ For the {{TaskName}} task, {{nb_runs}} run(s) of {{suffix}} {{PulseSequenceType}} {{ScanningSequence}} {{SequenceVariant}} fMRI data were collected. -The acquisition parameters were: {{nb_slices}} slices acquired in a {{so_str}} fashion; repetition time, TR= {{RepetitionTime}} s; echo time, TE= {{echo_time}} ms; flip angle, FA= {{FlipAngle}} deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}}; voxel size= {{vox_size}} mm; multiband factor= {{mb_str}}; in-plane acceleration factor= {pr_str}}. +The acquisition parameters were: {{nb_slices}} slices acquired in a {{so_str}} fashion; repetition time, TR= {{RepetitionTime}} s; echo time, TE= {{echo_time}} s; flip angle, FA= {{FlipAngle}} deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}}; voxel size= {{vox_size}} mm; multiband factor= {{mb_str}}; in-plane acceleration factor= {pr_str}}. Each run was {{length}} minutes in length, during which {{nb_vols}} functional volumes were acquired. diff --git a/templates/boilerplates/perf.tmp b/templates/boilerplates/perf.tmp index df3be01d..f5e80a72 100644 --- a/templates/boilerplates/perf.tmp +++ b/templates/boilerplates/perf.tmp @@ -1 +1 @@ -{{PulseSequenceType}} {{ScanningSequence}} {{SequenceVariant}} {{suffix}} perfusion MRI data were collected ({{nb_slices}} slices; repetition time, TR= {{RepetitionTime}} ms; echo time, TE= {{echo_time}} ms; flip angle, FA= {{FlipAngle}} deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}}; voxel size= {{vox_size}} mm) +{{PulseSequenceType}} {{ScanningSequence}} {{SequenceVariant}} {{suffix}} perfusion MRI data were collected ({{nb_slices}} slices; repetition time, TR= {{RepetitionTime}} ms; echo time, TE= {{echo_time}} s; flip angle, FA= {{FlipAngle}} deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}}; voxel size= {{vox_size}} mm) diff --git a/templates/boilerplates/physio.tmp b/templates/boilerplates/physio.tmp new file mode 100644 index 00000000..c6fda198 --- /dev/null +++ b/templates/boilerplates/physio.tmp @@ -0,0 +1 @@ +Physiological for {{Columns}} were recorded with a sampling frequency of {{SamplingFrequency}}. diff --git a/tests/.gitignore b/tests/.gitignore index f7168a0c..e2ec8938 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1,11 +1,9 @@ - *.tsv -*.json +*.png dummy_ds output -data/MoAEpilot/ data/ds*/ data/dummy/ data/asl* diff --git a/tests/Makefile b/tests/Makefile index ae6570fe..65dff9a3 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -4,8 +4,10 @@ clean: rm -rf bids-examples mkdir -p bids-examples/ + .PHONY: data data: clean sh create_dummy_data_set.sh + rm -rf bids-examples git clone https://github.com/bids-standard/bids-examples.git --depth 1 touch bids-examples/.gitkeep diff --git a/tests/README.md b/tests/README.md index 0d167cc4..d99e21db 100644 --- a/tests/README.md +++ b/tests/README.md @@ -54,9 +54,6 @@ git clone git://github.com/bids-standard/bids-examples.git --depth 1 #### Datasets with content -The function `download_moae_ds.m` will download a lightweight dataset from the -SPM website. - To get more complex data sets, to test things you can use datalad. ```bash diff --git a/tests/data/SurfaceData/sub-06_hemi-R_space-individual_den-native_midthickness.json b/tests/data/SurfaceData/sub-06_hemi-R_space-individual_den-native_midthickness.json deleted file mode 100644 index f1b4de4b..00000000 --- a/tests/data/SurfaceData/sub-06_hemi-R_space-individual_den-native_midthickness.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "Sources": [ - "sub-06/anat/sub-06_hemi-R_space-individual_den-native_pial.surf.gii", - "sub-06/anat/sub-06_hemi-R_space-individual_den-native_white.surf.gii" - ], - "SpatialRef": "sub-06/anat/sub-06_hemi-R_space-individual_den-native_midthickness.surf.gii", - "Density": { - "native": "High-density native surface mesh generated by Freesurfer", - "x32K": "32K vertices per hemisphere, coordinates aligned with fsLR space", - "x164K": "164K vertices per hemisphere" - } -} diff --git a/tests/data/SurfaceData/sub-06_hemi-R_space-individual_den-native_thickness.json b/tests/data/SurfaceData/sub-06_hemi-R_space-individual_den-native_thickness.json deleted file mode 100644 index a955102a..00000000 --- a/tests/data/SurfaceData/sub-06_hemi-R_space-individual_den-native_thickness.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Sources": "sub-06/surf/rh.thickness", - "SpatialRef": "sub-06/anat/sub-06_hemi-R_space-individual_den-native_midthickness.surf.gii", - "Density": { - "native": "High-density native surface mesh generated by Freesurfer", - "x32K": "32K vertices per hemisphere, coordinates aligned with fsLR space", - "x164K": "164K vertices per hemisphere" - } -} diff --git a/tests/data/SurfaceData/sub-06_space-individual_den-native_thickness.json b/tests/data/SurfaceData/sub-06_space-individual_den-native_thickness.json deleted file mode 100644 index df226e42..00000000 --- a/tests/data/SurfaceData/sub-06_space-individual_den-native_thickness.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "Sources": [ - "sub-06/anat/sub-06_hemi-L_space-individual_den-native_thickness.shape.gii", - "sub-06/anat/sub-06_hemi-L_space-individual_den-native_desc-cortexAtlas_mask.shape.gii", - "sub-06/anat/sub-06_hemi-R_space-individual_den-native_thickness.shape.gii", - "sub-06/anat/sub-06_hemi-R_space-individual_den-native_desc-cortexAtlas_mask.shape.gii" - ], - "SpatialRef": { - "CIFTI_STRUCTURE_CORTEX_LEFT": "sub-06/anat/sub-06_hemi-L_space-individual_den-native_midthickness.surf.gii", - "CIFTI_STRUCTURE_CORTEX_RIGHT": "sub-06/anat/sub-06_hemi-R_space-individual_den-native_midthickness.surf.gii" - }, - "Density": { - "native": "High-density native surface mesh generated by Freesurfer", - "x32K": "32K vertices per hemisphere, coordinates aligned with fsLR space", - "x164K": "164K vertices per hemisphere" - } -} diff --git a/tests/data/model/model-balloonanalogrisktask_smdl.json b/tests/data/model/model-balloonanalogrisktask_smdl.json new file mode 100644 index 00000000..9f58283a --- /dev/null +++ b/tests/data/model/model-balloonanalogrisktask_smdl.json @@ -0,0 +1,57 @@ +{ + "Name": "balloonanalogrisktask", + "BIDSModelVersion": "1.0.0", + "Description": "default model for balloonanalogrisktask", + "Input": { + "task": [ + "balloonanalogrisktask" + ] + }, + "Nodes": [ + { + "Level": "Run", + "Name": "run_level", + "GroupBy": [ + "run", + "subject" + ], + "Transformations": { + "Transformer": "bidspm", + "Instructions": [ + { + "Name": "Replace", + "Input": [ + "trial_type" + ], + "Output": [ + "renamed" + ], + "Replace": [ + { + "key": "pumps_demean", + "value": "pumps" + }, + { + "key": "control_pumps_demean", + "value": "control_pumps" + } + ], + "Attribute": "value" + } + ] + }, + "Model": { + "Type": "glm", + "X": [ + "renamed.pumps", + "renamed.control_pumps", + 1 + ], + "HRF": { + "Variables": [], + "Model": "spm" + } + } + } + ] +} diff --git a/tests/data/model/model-bug385_smdl.json b/tests/data/model/model-bug385_smdl.json new file mode 100644 index 00000000..18c58061 --- /dev/null +++ b/tests/data/model/model-bug385_smdl.json @@ -0,0 +1,131 @@ +{ + "Name": "default_motion_model", + "BIDSModelVersion": "1.0.0", + "Description": "bug: https://github.com/bids-standard/bids-matlab/issues/385", + "Input": { + "task": [ + "motion" + ], + "acq": [ + "" + ], + "space": [ + "MNI152NLin2009cAsym" + ] + }, + "Nodes": [ + { + "Level": "Run", + "Name": "run", + "GroupBy": [ + "run", + "session", + "subject" + ], + "Model": { + "X": [ + "trial_type.BioMot_LH", + "trial_type.BioMot_RH", + "trial_type.BioScr_LH", + "trial_type.BioScr_RH", + "trial_type.motion_LH", + "trial_type.motion_RH", + "trial_type.static_LH", + "trial_type.static_RH", + "1", + "trans_?", + "rot_?", + "non_steady_state_outlier*", + "motion_outlier*" + ], + "Type": "glm", + "HRF": { + "Variables": [ + "trial_type.BioMot_LH", + "trial_type.BioMot_RH", + "trial_type.BioScr_LH", + "trial_type.BioScr_RH", + "trial_type.motion_LH", + "trial_type.motion_RH", + "trial_type.static_LH", + "trial_type.static_RH" + ], + "Model": "DoubleGamma" + }, + "Options": { + "HighPassFilterCutoffHz": 0.008, + "Mask": { + "desc": [ + "brain" + ], + "suffix": [ + "mask" + ] + } + }, + "Software": { + "SPM": { + "SerialCorrelation": "FAST", + "HRFderivatives": "Temporal" + } + } + }, + "DummyContrasts": { + "Test": "t", + "Contrasts": [ + "trial_type.BioMot_LH", + "trial_type.BioMot_RH", + "trial_type.BioScr_LH", + "trial_type.BioScr_RH", + "trial_type.motion_LH", + "trial_type.motion_RH", + "trial_type.static_LH", + "trial_type.static_RH" + ] + } + }, + { + "Level": "Subject", + "Name": "subject", + "GroupBy": [ + "subject", + "contrast" + ], + "Model": { + "X": [ + 1 + ], + "Type": "glm" + }, + "DummyContrasts": { + "Test": "t" + } + }, + { + "Level": "Dataset", + "Name": "dataset", + "GroupBy": [ + "contrast" + ], + "Model": { + "X": [ + 1 + ], + "Type": "glm" + }, + "DummyContrasts": { + "Test": "t" + } + } + ], + "Edges": [ + { + "Source": "run", + "Destination": "subject" + }, + { + "Source": "subject", + "Destination": "dataset" + } + ] +} diff --git a/tests/data/model/model-empty_smdl.json b/tests/data/model/model-empty_smdl.json new file mode 100644 index 00000000..f6753fe1 --- /dev/null +++ b/tests/data/model/model-empty_smdl.json @@ -0,0 +1,73 @@ +{ + "Name": "empty_model", + "BIDSModelVersion": "1.0.0", + "Description": "This is an empty BIDS stats model.", + "Input": { + "task": [ + "" + ] + }, + "Nodes": [ + { + "Level": "Run", + "Name": "run", + "GroupBy": [ + "" + ], + "Transformations": { + "Transformer": "", + "Instructions": [ + { + "Name": "", + "Inputs": [ + "" + ] + } + ] + }, + "Model": { + "X": [ + "" + ], + "Type": "glm", + "HRF": { + "Variables": [ + "" + ], + "Model": "DoubleGamma" + }, + "Options": { + "HighPassFilterCutoffHz": 0.008, + "Mask": { + "desc": [ + "brain" + ], + "suffix": [ + "mask" + ] + } + }, + "Software": [] + }, + "Contrasts": [ + { + "Name": "", + "ConditionList": [ + "" + ], + "Weights": [ + "" + ], + "Test": "t" + } + ], + "DummyContrasts": { + "Test": "t", + "Contrasts": [ + "" + ] + } + } + ], + "Edges": [] +} diff --git a/tests/data/model/model-narps_smdl.json b/tests/data/model/model-narps_smdl.json new file mode 100644 index 00000000..b45e598f --- /dev/null +++ b/tests/data/model/model-narps_smdl.json @@ -0,0 +1,201 @@ +{ + "Name": "NARPS", + "Description": "NARPS Analysis model", + "BIDSModelVersion": "1.0.0", + "Input": { + "task": [ + "MGT" + ] + }, + "Nodes": [ + { + "Level": "Run", + "Name": "run", + "GroupBy": [ + "run", + "subject" + ], + "Transformations": { + "Transformer": "pybids-transforms-v1", + "Instructions": [ + { + "Name": "Threshold", + "Input": [ + "gain" + ], + "Binarize": true, + "Output": [ + "trials" + ] + }, + { + "Name": "Scale", + "Input": [ + "gain", + "loss", + "RT" + ], + "Demean": true, + "Rescale": false, + "Output": [ + "gain", + "loss", + "demeaned_RT" + ] + }, + { + "Name": "Convolve", + "Model": "spm", + "Input": [ + "trials", + "gain", + "loss", + "demeaned_RT" + ] + } + ] + }, + "Model": { + "X": [ + "trials", + "gain", + "loss", + "demeaned_RT", + "rot_x", + "rot_y", + "rot_z", + "trans_x", + "trans_y", + "trans_z", + 1 + ], + "Type": "glm" + }, + "DummyContrasts": { + "Conditions": [ + "trials", + "gain", + "loss" + ], + "Test": "t" + } + }, + { + "Level": "Subject", + "Name": "subject", + "GroupBy": [ + "subject", + "contrast" + ], + "Model": { + "X": [ + 1 + ], + "Type": "meta" + }, + "DummyContrasts": { + "Test": "t" + } + }, + { + "Level": "Dataset", + "Name": "between-groups", + "GroupBy": [ + "contrast" + ], + "Model": { + "Type": "glm", + "X": [ + 1, + "group" + ], + "Formula": "0 + C(group)" + }, + "Contrasts": [ + { + "Name": "range_vs_indiference", + "ConditionList": [ + "C(group)[T.equalRange]", + "C(group)[T.equalIndifference]" + ], + "Weights": [ + 1, + -1 + ], + "Test": "t" + } + ] + }, + { + "Level": "Dataset", + "Name": "positive", + "GroupBy": [ + "contrast", + "group" + ], + "Model": { + "Type": "glm", + "X": [ + 1 + ] + }, + "DummyContrasts": { + "Test": "t" + } + }, + { + "Level": "Dataset", + "Name": "negative-loss", + "GroupBy": [ + "contrast", + "group" + ], + "Model": { + "Type": "glm", + "X": [ + 1 + ] + }, + "Contrasts": [ + { + "Name": "negative", + "ConditionList": [ + 1 + ], + "Weights": [ + -1 + ], + "Test": "t" + } + ] + } + ], + "Edges": [ + { + "Source": "run", + "Destination": "subject" + }, + { + "Source": "subject", + "Destination": "positive" + }, + { + "Source": "subject", + "Destination": "negative-loss", + "Filter": { + "contrast": [ + "loss" + ] + } + }, + { + "Source": "subject", + "Destination": "between-groups", + "Filter": { + "contrast": [ + "loss" + ] + } + } + ] +} diff --git a/tests/data/model/model-rhymejudgement_smdl.json b/tests/data/model/model-rhymejudgement_smdl.json new file mode 100644 index 00000000..10f0c6cf --- /dev/null +++ b/tests/data/model/model-rhymejudgement_smdl.json @@ -0,0 +1,173 @@ +{ + "Name": "default_rhymejudgment_model", + "BIDSModelVersion": "1.0.0", + "Description": "default BIDS stats model for rhymejudgment task", + "Input": { + "task": [ + "rhymejudgment" + ] + }, + "Nodes": [ + { + "Level": "Run", + "Name": "run", + "GroupBy": [ + "run", + "subject" + ], + "Transformations": { + "Transformer": "", + "Instructions": [ + { + "Name": "", + "Inputs": [ + "" + ] + } + ] + }, + "Model": { + "X": [ + "trial_type.pseudoword", + "trial_type.word", + "1" + ], + "Type": "glm", + "HRF": { + "Variables": [ + "trial_type.pseudoword", + "trial_type.word" + ], + "Model": "DoubleGamma" + }, + "Options": { + "HighPassFilterCutoffHz": 0.008, + "Mask": { + "desc": [ + "brain" + ], + "suffix": [ + "mask" + ] + } + }, + "Software": [] + }, + "Contrasts": [ + { + "Name": "", + "ConditionList": [ + "" + ], + "Weights": [ + "" + ], + "Test": "t" + } + ], + "DummyContrasts": { + "Test": "t", + "Contrasts": [ + "trial_type.pseudoword", + "trial_type.word" + ] + } + }, + { + "Level": "Subject", + "Name": "subject", + "GroupBy": [ + "" + ], + "Model": { + "X": [ + "" + ], + "Type": "glm", + "Options": { + "HighPassFilterCutoffHz": 0.008, + "Mask": { + "desc": [ + "brain" + ], + "suffix": [ + "mask" + ] + } + }, + "Software": [] + }, + "Contrasts": [ + { + "Name": "", + "ConditionList": [ + "" + ], + "Weights": [ + "" + ], + "Test": "t" + } + ], + "DummyContrasts": { + "Test": "t", + "Contrasts": [ + "" + ] + } + }, + { + "Level": "Dataset", + "Name": "dataset", + "GroupBy": [ + "" + ], + "Model": { + "X": [ + "" + ], + "Type": "glm", + "Options": { + "HighPassFilterCutoffHz": 0.008, + "Mask": { + "desc": [ + "brain" + ], + "suffix": [ + "mask" + ] + } + }, + "Software": [] + }, + "Contrasts": [ + { + "Name": "", + "ConditionList": [ + "" + ], + "Weights": [ + "" + ], + "Test": "t" + } + ], + "DummyContrasts": { + "Test": "t", + "Contrasts": [ + "" + ] + } + } + ], + "Edges": [ + { + "Source": "run", + "Destination": "subject" + }, + { + "Source": "subject", + "Destination": "dataset" + } + ] +} diff --git a/tests/data/reports/MoAE_all.md b/tests/data/reports/MoAE_all.md index d6456992..8ae06aa1 100644 --- a/tests/data/reports/MoAE_all.md +++ b/tests/data/reports/MoAE_all.md @@ -1,4 +1,10 @@ -ANAT REPORT + +# Data description + + +### Anatomical Magnetic Resonance Imaging data + +#### T1-weighted image The data acquisition was performed in the {{InstitutionName}}, {{InstitutionalDepartmentName}}, {{InstitutionAddress}}. @@ -9,11 +15,14 @@ The software version was {{SoftwareVersions}}. {{PulseSequenceType}} {{ScanningSequence}} {{SequenceVariant}} T1w structural MRI data were collected (54 slices; repetition time, TR= {{RepetitionTime}} s; echo -time, TE= {{echo_time}} ms; flip angle, FA= {{FlipAngle}} deg; field of view, -FOV= 256.00 X 256.00 mm; matrix size= 256 X 256; voxel size= 1.00 X 1.00 X 3.00 -mm). +time, TE= {{echo_time}} s; flip angle, FA= {{FlipAngle}} deg; field of view, FOV= +256 X 256 mm; matrix size= 256 X 256; voxel size= 1.00 X 1.00 X 3.00 mm). -FUNC REPORT +### Task-Based Magnetic Resonance Imaging data + +#### BOLD data + +##### Task auditory data The data acquisition was performed in the {{InstitutionName}}, {{InstitutionalDepartmentName}}, {{InstitutionAddress}}. @@ -24,11 +33,13 @@ with serial number {{DeviceSerialNumber}}. The software version was {{SoftwareVe For the Auditory task, 1 run(s) of bold {{PulseSequenceType}} {{ScanningSequence}} {{SequenceVariant}} fMRI data were collected. The acquisition parameters were: 64 slices acquired in a {{so_str}} fashion; repetition time, TR= 7 s; echo time, -TE= {{echo_time}} ms; flip angle, FA= {{FlipAngle}} deg; field of view, FOV= 192.00 -X 192.00 mm; matrix size= 64 X 64; voxel size= 3.00 X 3.00 X 3.00 mm; multiband -factor= {{mb_str}}; in-plane acceleration factor= {pr_str}}. Each run was -9.8 minutes in length, during which 84 functional volumes were acquired. +TE= {{echo_time}} s; flip angle, FA= {{FlipAngle}} deg; field of view, FOV= 192 +X 192 mm; matrix size= 64 X 64; voxel size= 3.00 X 3.00 X 3.00 mm; multiband +factor= {{mb_str}}; in-plane acceleration factor= {pr_str}}. Each run was 9.8 minutes +in length, during which 84 functional volumes were acquired. {{TaskDescription}} Participants were specifically instructed to: {{Instructions}} +###### events data + This text was automatically generated by [BIDS-matlab](https://github.com/bids-standard/bids-matlab). diff --git a/tests/data/reports/asl003_perf.md b/tests/data/reports/asl003_perf.md index 1d370b35..4f9c2160 100644 --- a/tests/data/reports/asl003_perf.md +++ b/tests/data/reports/asl003_perf.md @@ -1,4 +1,10 @@ -PERF REPORT + +# Data description + + +### Perfusion imaging data + +#### Arterial Spin Labeling The data acquisition was performed in the {{InstitutionName}}, {{InstitutionalDepartmentName}}, {{InstitutionAddress}}. @@ -7,9 +13,11 @@ MRI scans were acquired at 3 Tesla using a TrioTim system from Siemens, with ser number {{DeviceSerialNumber}}. The software version was N4_VB17A_LATEST_20090307. 3Dgrase RM SK asl perfusion MRI data were collected ({{nb_slices}} slices; repetition -time, TR= {{RepetitionTime}} ms; echo time, TE= {{echo_time}} ms; flip angle, -FA= 180 deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}}; voxel -size= {{vox_size}} mm) +time, TR= {{RepetitionTime}} ms; echo time, TE= 0.01192 s; flip angle, FA= +180 deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}}; voxel size= +{{vox_size}} mm) + +#### Arterial Spin Labeling Context The data acquisition was performed in the {{InstitutionName}}, {{InstitutionalDepartmentName}}, {{InstitutionAddress}}. @@ -20,10 +28,14 @@ The software version was {{SoftwareVersions}}. {{PulseSequenceType}} {{ScanningSequence}} {{SequenceVariant}} aslcontext perfusion MRI data were collected ({{nb_slices}} slices; repetition time, TR= {{RepetitionTime}} -ms; echo time, TE= {{echo_time}} ms; flip angle, FA= {{FlipAngle}} deg; +ms; echo time, TE= {{echo_time}} s; flip angle, FA= {{FlipAngle}} deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}}; voxel size= {{vox_size}} mm) +#### ASL Labeling Screenshot + +#### M0 image + The data acquisition was performed in the {{InstitutionName}}, {{InstitutionalDepartmentName}}, {{InstitutionAddress}}. @@ -31,8 +43,8 @@ MRI scans were acquired at 3 Tesla using a TrioTim system from Siemens, with ser number {{DeviceSerialNumber}}. The software version was N4_VB17A_LATEST_20090307. 3Dgrase RM SK m0scan perfusion MRI data were collected ({{nb_slices}} slices; repetition -time, TR= {{RepetitionTime}} ms; echo time, TE= {{echo_time}} ms; flip -angle, FA= 180 deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}}; voxel -size= {{vox_size}} mm) +time, TR= {{RepetitionTime}} ms; echo time, TE= 0.01614 s; flip angle, +FA= 180 deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}}; voxel size= +{{vox_size}} mm) This text was automatically generated by [BIDS-matlab](https://github.com/bids-standard/bids-matlab). diff --git a/tests/data/reports/ds000117_anat.md b/tests/data/reports/ds000117_anat.md index d5c129c8..6c069aef 100644 --- a/tests/data/reports/ds000117_anat.md +++ b/tests/data/reports/ds000117_anat.md @@ -1,4 +1,10 @@ -ANAT REPORT + +# Data description + + +### Anatomical Magnetic Resonance Imaging data + +#### T1-weighted image The data acquisition was performed in the MRC Cognition and Brain Sciences Unit, {{InstitutionalDepartmentName}}, 15 Chaucer Road, Cambridge CB2 7EF, UK. @@ -7,8 +13,8 @@ MRI scans were acquired at 3 Tesla using a TrioTim system from Siemens, with ser number 35119. The software version was syngo_MR_B15. MPRAGE GR_IR SP_MP_OSP T1w structural MRI data were collected ({{nb_slices}} slices; -repetition time, TR= 2.25 s; echo time, TE= {{echo_time}} ms; flip angle, FA= -9 deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}}; voxel size= -{{vox_size}} mm). +repetition time, TR= 2.25 s; echo time, TE= 0.00298 s; flip angle, FA= 9 deg; +field of view, FOV= {{fov}} mm; matrix size= {{mat_size}}; voxel size= {{vox_size}} +mm). This text was automatically generated by [BIDS-matlab](https://github.com/bids-standard/bids-matlab). diff --git a/tests/data/reports/ds000117_dwi.md b/tests/data/reports/ds000117_dwi.md index 1288c075..d1f2d561 100644 --- a/tests/data/reports/ds000117_dwi.md +++ b/tests/data/reports/ds000117_dwi.md @@ -1,4 +1,10 @@ -DWI REPORT + +# Data description + + +### Diffusion-Weighted Imaging data + +#### Diffusion-weighted image The data acquisition was performed in the {{InstitutionName}}, {{InstitutionalDepartmentName}}, {{InstitutionAddress}}. @@ -9,9 +15,9 @@ The software version was {{SoftwareVersions}}. 1 run of {{PulseSequenceType}} {{ScanningSequence}} {{SequenceVariant}} diffusion-weighted-imaging (dwi) data were collected ({{nb_slices}} slices ; repetition -time, TR= {{RepetitionTime}} s; echo time, TE= {{echo_time}} ms; flip angle, FA= -{{FlipAngle}} deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}} ; -voxel size= {{vox_size}} mm ; b-values of XXX acquired; XXX diffusion directions; +time, TR= {{RepetitionTime}} s; echo time, TE= {{echo_time}} s; flip angle, FA= +{{FlipAngle}} deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}} ; voxel +size= {{vox_size}} mm ; b-values of XXX acquired; XXX diffusion directions; multiband factor= {{mb_str}} ). This text was automatically generated by [BIDS-matlab](https://github.com/bids-standard/bids-matlab). diff --git a/tests/data/reports/ds000117_fmap.md b/tests/data/reports/ds000117_fmap.md index 92badf58..684fefb8 100644 --- a/tests/data/reports/ds000117_fmap.md +++ b/tests/data/reports/ds000117_fmap.md @@ -1,4 +1,10 @@ -FMAP REPORT + +# Data description + + +### Field maps data + +#### Magnitude The data acquisition was performed in the {{InstitutionName}}, {{InstitutionalDepartmentName}}, {{InstitutionAddress}}. @@ -9,10 +15,12 @@ The software version was {{SoftwareVersions}}. A {{PulseSequenceType}} {{ScanningSequence}} {{SequenceVariant}} field map (phase encoding: {{PhaseEncodingDirection}}; {{nb_slices}} slices; repetition time, TR= -{{RepetitionTime}} s; echo time 1 / 2, TE 1/2= {{echo_time}} ms; flip angle, -FA= {{FlipAngle}} deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}}; +{{RepetitionTime}} s; echo time 1 / 2, TE 1/2= {{echo_time}} s; flip angle, FA= +{{FlipAngle}} deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}}; voxel size= {{vox_size}} mm) was acquired. +#### Magnitude + The data acquisition was performed in the {{InstitutionName}}, {{InstitutionalDepartmentName}}, {{InstitutionAddress}}. @@ -22,10 +30,12 @@ The software version was {{SoftwareVersions}}. A {{PulseSequenceType}} {{ScanningSequence}} {{SequenceVariant}} field map (phase encoding: {{PhaseEncodingDirection}}; {{nb_slices}} slices; repetition time, TR= -{{RepetitionTime}} s; echo time 1 / 2, TE 1/2= {{echo_time}} ms; flip angle, -FA= {{FlipAngle}} deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}}; +{{RepetitionTime}} s; echo time 1 / 2, TE 1/2= {{echo_time}} s; flip angle, FA= +{{FlipAngle}} deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}}; voxel size= {{vox_size}} mm) was acquired. +#### Phase-difference + The data acquisition was performed in the {{InstitutionName}}, {{InstitutionalDepartmentName}}, {{InstitutionAddress}}. @@ -35,7 +45,7 @@ The software version was {{SoftwareVersions}}. A {{PulseSequenceType}} {{ScanningSequence}} {{SequenceVariant}} field map (phase encoding: {{PhaseEncodingDirection}}; {{nb_slices}} slices; repetition time, TR= -0.4 s; echo time 1 / 2, TE 1/2= 0.01 / 0.01 ms; flip angle, FA= 60 deg; field +0.4 s; echo time 1 / 2, TE 1/2= 0.01 / 0.01 s; flip angle, FA= 60 deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}}; voxel size= {{vox_size}} mm) was acquired. diff --git a/tests/data/reports/ds000117_func.md b/tests/data/reports/ds000117_func.md index 3af58bee..fb61d8b6 100644 --- a/tests/data/reports/ds000117_func.md +++ b/tests/data/reports/ds000117_func.md @@ -1,4 +1,12 @@ -FUNC REPORT + +# Data description + + +### Task-Based Magnetic Resonance Imaging data + +#### BOLD data + +##### Task facerecognition data The data acquisition was performed in the MRC Cognition and Brain Sciences Unit, {{InstitutionalDepartmentName}}, 15 Chaucer Road, Cambridge CB2 7EF, UK. @@ -6,9 +14,9 @@ The data acquisition was performed in the MRC Cognition and Brain Sciences Unit, MRI scans were acquired at 3 Tesla using a TrioTim system from Siemens, with serial number 35119. The software version was syngo_MR_B15. -For the facerecognition task, 9 run(s) of events EPI EP SK fMRI data were collected. -The acquisition parameters were: {{nb_slices}} slices acquired in a {{so_str}} -fashion; repetition time, TR= 2 s; echo time, TE= {{echo_time}} ms; flip angle, +For the facerecognition task, 9 run(s) of bold EPI EP SK fMRI data were collected. +The acquisition parameters were: {{nb_slices}} slices acquired in a interleaved +ascending fashion; repetition time, TR= 2 s; echo time, TE= 0.03 s; flip angle, FA= 78 deg; field of view, FOV= {{fov}} mm; matrix size= {{mat_size}}; voxel size= {{vox_size}} mm; multiband factor= {{mb_str}}; in-plane acceleration factor= {pr_str}}. Each run was {{length}} minutes in length, during which {{nb_vols}} @@ -16,4 +24,6 @@ functional volumes were acquired. {{TaskDescription}} Participants were specifically instructed to: {{Instructions}} +###### events data + This text was automatically generated by [BIDS-matlab](https://github.com/bids-standard/bids-matlab). diff --git a/tests/data/reports/ds000117_meg.md b/tests/data/reports/ds000117_meg.md index 93d1b0ce..5e75b981 100644 --- a/tests/data/reports/ds000117_meg.md +++ b/tests/data/reports/ds000117_meg.md @@ -1,4 +1,16 @@ -MEG REPORT + +# Data description + + +### Magnetoencephalography data + +#### EVENTS data + +#### HEADSHAPE data + +#### MEG data + +##### Task facerecognition data The data acquisition was performed in the MRC Cognition & Brain Sciences Unit, {{InstitutionalDepartmentName}}, 15 Chaucer Road, Cambridge, UK. @@ -13,4 +25,6 @@ recorded simultaneously. {{TaskDescription}} Participants were specifically instructed to: {{Instructions}} +###### events data + This text was automatically generated by [BIDS-matlab](https://github.com/bids-standard/bids-matlab). diff --git a/tests/data/reports/pet001_pet.md b/tests/data/reports/pet001_pet.md index 95d683b5..ba89535f 100644 --- a/tests/data/reports/pet001_pet.md +++ b/tests/data/reports/pet001_pet.md @@ -1,4 +1,12 @@ -PET REPORT + +# Data description + + +### Positron Emission Tomography data + +#### Blood recording data + +#### Positron Emission Tomography The data acquisition was performed in the {{InstitutionName}}, {{InstitutionalDepartmentName}}, {{InstitutionAddress}}. diff --git a/tests/data/synthetic/derivatives/invalid_subfolder/dataset_description.json b/tests/data/synthetic/derivatives/invalid_subfolder/dataset_description.json new file mode 100644 index 00000000..9a41fdfe --- /dev/null +++ b/tests/data/synthetic/derivatives/invalid_subfolder/dataset_description.json @@ -0,0 +1,10 @@ +{ + "BIDSVersion": "1.7.0", + "Name": "For testing 'invalid' layout", + "DatasetType": "derivative", + "GeneratedBy": [ + { + "Name": "manual" + } + ] +} diff --git a/tests/data/synthetic/derivatives/manual/dataset_description.json b/tests/data/synthetic/derivatives/manual/dataset_description.json new file mode 100644 index 00000000..9a41fdfe --- /dev/null +++ b/tests/data/synthetic/derivatives/manual/dataset_description.json @@ -0,0 +1,10 @@ +{ + "BIDSVersion": "1.7.0", + "Name": "For testing 'invalid' layout", + "DatasetType": "derivative", + "GeneratedBy": [ + { + "Name": "manual" + } + ] +} diff --git a/tests/data/synthetic/sub-ctrl01/anat/sub-ctrl01_T1w.nii.gz b/tests/data/synthetic/sub-ctrl01/anat/sub-ctrl01_T1w.nii.gz new file mode 100644 index 00000000..e69de29b diff --git a/tests/data/synthetic/sub-test01/anat/sub-test01_T1w.nii.gz b/tests/data/synthetic/sub-test01/anat/sub-test01_T1w.nii.gz new file mode 100644 index 00000000..e69de29b diff --git a/tests/data/tsv_files/sub-01_task-FaceRepetitionAfter_events.tsv b/tests/data/tsv_files/sub-01_task-FaceRepetitionAfter_events.tsv new file mode 100644 index 00000000..1f383010 --- /dev/null +++ b/tests/data/tsv_files/sub-01_task-FaceRepetitionAfter_events.tsv @@ -0,0 +1,105 @@ +onset duration trial_type +-6.66666666666732E-05 0 F1 +4.49993333333333 0 F1 +13.4999333333333 0 N1 +17.9999333333333 0 F1 +22.4999333333333 0 F1 +26.9999333333333 0 N2 +31.4999333333333 0 N1 +35.9999333333333 0 N1 +44.9999333333333 0 F1 +53.9999333333333 0 N1 +58.4999333333333 0 N1 +62.9999333333333 0 N1 +67.4999333333333 0 F2 +71.9999333333333 0 N1 +80.9999333333333 0 N2 +85.4999333333333 0 N1 +89.9999333333333 0 F1 +94.4999333333333 0 N2 +98.9999333333333 0 F2 +103.499933333333 0 F1 +112.499933333333 0 N2 +121.499933333333 0 F1 +125.999933333333 0 F1 +130.499933333333 0 N1 +134.999933333333 0 N1 +148.499933333333 0 N1 +152.999933333333 0 F1 +157.499933333333 0 F1 +170.999933333333 0 F1 +179.999933333333 0 N2 +184.499933333333 0 N1 +188.999933333333 0 N2 +193.499933333333 0 N2 +197.999933333333 0 F1 +202.499933333333 0 F1 +206.999933333333 0 F1 +211.499933333333 0 F2 +224.999933333333 0 N1 +233.999933333333 0 F1 +238.499933333333 0 N1 +247.499933333333 0 N1 +251.999933333333 0 N1 +260.999933333333 0 F1 +269.999933333333 0 N2 +274.499933333333 0 N1 +283.499933333333 0 N1 +287.999933333333 0 N1 +292.499933333333 0 N1 +296.999933333333 0 N2 +301.499933333333 0 F1 +305.999933333333 0 F2 +310.499933333333 0 N1 +314.999933333333 0 F2 +319.499933333333 0 N1 +323.999933333333 0 N1 +328.499933333333 0 N1 +337.499933333333 0 F2 +341.999933333333 0 F1 +355.499933333333 0 F2 +359.999933333333 0 F2 +364.499933333333 0 F2 +368.999933333333 0 N2 +377.999933333333 0 F1 +382.499933333333 0 N2 +395.999933333333 0 F2 +404.999933333333 0 N2 +409.499933333333 0 N1 +431.999933333333 0 N2 +445.499933333333 0 F2 +454.499933333333 0 F1 +467.999933333333 0 N2 +472.499933333333 0 N2 +476.999933333333 0 N1 +481.499933333333 0 F2 +485.999933333333 0 N2 +490.499933333333 0 N2 +508.499933333333 0 F2 +512.999933333333 0 N2 +521.999933333333 0 N2 +530.999933333333 0 F1 +535.499933333333 0 F2 +539.999933333333 0 F2 +548.999933333333 0 F2 +562.499933333333 0 N2 +566.999933333333 0 F1 +571.499933333333 0 F1 +575.999933333333 0 F1 +580.499933333333 0 N2 +589.499933333333 0 F2 +598.499933333333 0 F2 +602.999933333333 0 F2 +607.499933333333 0 N2 +620.999933333333 0 N2 +629.999933333333 0 F2 +634.499933333333 0 F2 +638.999933333333 0 N2 +652.499933333333 0 F2 +665.999933333333 0 F2 +670.499933333333 0 F2 +674.999933333333 0 F2 +679.499933333333 0 N2 +683.999933333333 0 N2 +688.499933333333 0 F1 +692.999933333333 0 F2 diff --git a/tests/data/tsv_files/sub-01_task-FaceRepetitionBefore_events.tsv b/tests/data/tsv_files/sub-01_task-FaceRepetitionBefore_events.tsv new file mode 100644 index 00000000..b305affb --- /dev/null +++ b/tests/data/tsv_files/sub-01_task-FaceRepetitionBefore_events.tsv @@ -0,0 +1,105 @@ +onset duration trial_type repetition_type face_type +-6.66666666666732E-05 0 face 1 famous +4.49993333333333 0 face 1 famous +13.4999333333333 0 face 1 unfamiliar +17.9999333333333 0 face 1 famous +22.4999333333333 0 face 1 famous +26.9999333333333 0 face 2 unfamiliar +31.4999333333333 0 face 1 unfamiliar +35.9999333333333 0 face 1 unfamiliar +44.9999333333333 0 face 1 famous +53.9999333333333 0 face 1 unfamiliar +58.4999333333333 0 face 1 unfamiliar +62.9999333333333 0 face 1 unfamiliar +67.4999333333333 0 face 2 famous +71.9999333333333 0 face 1 unfamiliar +80.9999333333333 0 face 2 unfamiliar +85.4999333333333 0 face 1 unfamiliar +89.9999333333333 0 face 1 famous +94.4999333333333 0 face 2 unfamiliar +98.9999333333333 0 face 2 famous +103.499933333333 0 face 1 famous +112.499933333333 0 face 2 unfamiliar +121.499933333333 0 face 1 famous +125.999933333333 0 face 1 famous +130.499933333333 0 face 1 unfamiliar +134.999933333333 0 face 1 unfamiliar +148.499933333333 0 face 1 unfamiliar +152.999933333333 0 face 1 famous +157.499933333333 0 face 1 famous +170.999933333333 0 face 1 famous +179.999933333333 0 face 2 unfamiliar +184.499933333333 0 face 1 unfamiliar +188.999933333333 0 face 2 unfamiliar +193.499933333333 0 face 2 unfamiliar +197.999933333333 0 face 1 famous +202.499933333333 0 face 1 famous +206.999933333333 0 face 1 famous +211.499933333333 0 face 2 famous +224.999933333333 0 face 1 unfamiliar +233.999933333333 0 face 1 famous +238.499933333333 0 face 1 unfamiliar +247.499933333333 0 face 1 unfamiliar +251.999933333333 0 face 1 unfamiliar +260.999933333333 0 face 1 famous +269.999933333333 0 face 2 unfamiliar +274.499933333333 0 face 1 unfamiliar +283.499933333333 0 face 1 unfamiliar +287.999933333333 0 face 1 unfamiliar +292.499933333333 0 face 1 unfamiliar +296.999933333333 0 face 2 unfamiliar +301.499933333333 0 face 1 famous +305.999933333333 0 face 2 famous +310.499933333333 0 face 1 unfamiliar +314.999933333333 0 face 2 famous +319.499933333333 0 face 1 unfamiliar +323.999933333333 0 face 1 unfamiliar +328.499933333333 0 face 1 unfamiliar +337.499933333333 0 face 2 famous +341.999933333333 0 face 1 famous +355.499933333333 0 face 2 famous +359.999933333333 0 face 2 famous +364.499933333333 0 face 2 famous +368.999933333333 0 face 2 unfamiliar +377.999933333333 0 face 1 famous +382.499933333333 0 face 2 unfamiliar +395.999933333333 0 face 2 famous +404.999933333333 0 face 2 unfamiliar +409.499933333333 0 face 1 unfamiliar +431.999933333333 0 face 2 unfamiliar +445.499933333333 0 face 2 famous +454.499933333333 0 face 1 famous +467.999933333333 0 face 2 unfamiliar +472.499933333333 0 face 2 unfamiliar +476.999933333333 0 face 1 unfamiliar +481.499933333333 0 face 2 famous +485.999933333333 0 face 2 unfamiliar +490.499933333333 0 face 2 unfamiliar +508.499933333333 0 face 2 famous +512.999933333333 0 face 2 unfamiliar +521.999933333333 0 face 2 unfamiliar +530.999933333333 0 face 1 famous +535.499933333333 0 face 2 famous +539.999933333333 0 face 2 famous +548.999933333333 0 face 2 famous +562.499933333333 0 face 2 unfamiliar +566.999933333333 0 face 1 famous +571.499933333333 0 face 1 famous +575.999933333333 0 face 1 famous +580.499933333333 0 face 2 unfamiliar +589.499933333333 0 face 2 famous +598.499933333333 0 face 2 famous +602.999933333333 0 face 2 famous +607.499933333333 0 face 2 unfamiliar +620.999933333333 0 face 2 unfamiliar +629.999933333333 0 face 2 famous +634.499933333333 0 face 2 famous +638.999933333333 0 face 2 unfamiliar +652.499933333333 0 face 2 famous +665.999933333333 0 face 2 famous +670.499933333333 0 face 2 famous +674.999933333333 0 face 2 famous +679.499933333333 0 face 2 unfamiliar +683.999933333333 0 face 2 unfamiliar +688.499933333333 0 face 1 famous +692.999933333333 0 face 2 famous diff --git a/tests/data/tsv_files/sub-01_task-TouchAfter_events.tsv b/tests/data/tsv_files/sub-01_task-TouchAfter_events.tsv new file mode 100644 index 00000000..902c2041 --- /dev/null +++ b/tests/data/tsv_files/sub-01_task-TouchAfter_events.tsv @@ -0,0 +1,348 @@ +onset duration trial_type direction soundTarget fixationTarget event block keyName +6.333479000000001 1 static -1 0 0 1 1 n/a +7.361279000000001 1 static -2 0 0 2 1 n/a +9.391801000000001 1 static -1 0 0 3 1 n/a +10.410077 1 static -2 0 0 4 1 n/a +12.44284 1 static -1 0 0 5 1 n/a +13.462703 1 static -2 0 0 6 1 n/a +15.482433 1 static -1 0 0 7 1 n/a +16.500075 1 static -2 0 0 8 1 n/a +18.535691 1 static -1 0 0 9 1 n/a +19.567618 1 static -2 0 0 10 1 n/a +21.58731 1 static -1 0 1 11 1 n/a +22.604634 1 static -1 0 1 12 1 n/a +24.638696 1 static -1 0 0 13 1 n/a +23.081176 0 response n/a n/a n/a n/a n/a b +25.665861 1 static -2 0 0 14 1 n/a +27.690053 1 static -1 0 0 15 1 n/a +28.725471 1 static -2 0 0 16 1 n/a +36.615386 1 motion 180 0 0 1 2 n/a +37.633094 1 motion 0 0 0 2 2 n/a +39.68088299999999 1 motion 90 0 0 3 2 n/a +40.717185 1 motion 270 0 0 4 2 n/a +42.734862 1 motion 180 0 1 5 2 n/a +43.754159 1 motion 180 0 1 6 2 n/a +45.788659 1 motion 90 0 0 7 2 n/a +44.401165 0 response n/a n/a n/a n/a n/a b +46.809724 1 motion 270 0 0 8 2 n/a +48.839513 1 motion 180 0 0 9 2 n/a +49.856904 1 motion 0 0 0 10 2 n/a +51.874254 1 motion 90 0 1 11 2 n/a +52.894049 1 motion 90 0 1 12 2 n/a +54.91108800000001 1 motion 180 0 0 13 2 n/a +54.025141 0 response n/a n/a n/a n/a n/a b +55.932924 1 motion 0 0 0 14 2 n/a +57.982622 1 motion 90 0 0 15 2 n/a +58.999561 1 motion 270 0 0 16 2 n/a +66.562337 1 static -1 0 0 1 3 n/a +67.607596 1 static -2 0 0 2 3 n/a +69.640672 1 static -1 0 0 3 3 n/a +70.658314 1 static -2 0 0 4 3 n/a +72.678043 1 static -1 0 0 5 3 n/a +73.69663700000001 1 static -2 0 0 6 3 n/a +75.729668 1 static -1 0 1 7 3 n/a +76.763181 1 static -1 0 1 8 3 n/a +78.78232799999999 1 static -1 0 0 9 3 n/a +77.257154 0 response n/a n/a n/a n/a n/a b +79.80155599999999 1 static -2 0 0 10 3 n/a +81.833032 1 static -1 0 0 11 3 n/a +82.853847 1 static -2 0 0 12 3 n/a +84.884967 1 static -1 0 1 13 3 n/a +85.90610000000001 1 static -1 0 1 14 3 n/a +87.938895 1 static -1 0 0 15 3 n/a +86.513138 0 response n/a n/a n/a n/a n/a b +88.96320300000001 1 static -2 0 0 16 3 n/a +97.46897199999999 1 motion 180 0 0 1 4 n/a +98.49684000000001 1 motion 0 0 0 2 4 n/a +100.515153 1 motion 90 0 1 3 4 n/a +101.532726 1 motion 90 0 1 4 4 n/a +103.55071 1 motion 180 0 0 5 4 n/a +102.241165 0 response n/a n/a n/a n/a n/a b +104.590006 1 motion 0 0 0 6 4 n/a +106.622863 1 motion 90 0 0 7 4 n/a +107.640119 1 motion 270 0 0 8 4 n/a +109.672752 1 motion 180 0 0 9 4 n/a +110.689509 1 motion 0 0 0 10 4 n/a +112.724639 1 motion 90 0 1 11 4 n/a +113.761576 1 motion 90 0 1 12 4 n/a +115.795126 1 motion 180 0 0 13 4 n/a +114.377109 0 response n/a n/a n/a n/a n/a b +116.813153 1 motion 0 0 0 14 4 n/a +118.849506 1 motion 90 0 0 15 4 n/a +119.867397 1 motion 270 0 0 16 4 n/a +128.598072 1 static -1 0 0 1 5 n/a +129.64111 1 static -2 0 0 2 5 n/a +131.675448 1 static -1 0 0 3 5 n/a +132.694993 1 static -2 0 0 4 5 n/a +134.727743 1 static -1 0 0 5 5 n/a +135.745702 1 static -2 0 0 6 5 n/a +137.76412 1 static -1 0 1 7 5 n/a +138.813825 1 static -1 0 1 8 5 n/a +140.833921 1 static -1 0 0 9 5 n/a +141.851562 1 static -2 0 0 10 5 n/a +143.884628 1 static -1 0 0 11 5 n/a +144.90354 1 static -2 0 0 12 5 n/a +146.938201 1 static -1 0 0 13 5 n/a +147.956794 1 static -2 0 0 14 5 n/a +149.992083 1 static -1 0 0 15 5 n/a +151.030359 1 static -2 0 0 16 5 n/a +158.083793 1 motion 180 0 0 1 6 n/a +159.114835 1 motion 0 0 0 2 6 n/a +161.134042 1 motion 90 0 0 3 6 n/a +162.151616 1 motion 270 0 0 4 6 n/a +164.185207 1 motion 180 0 0 5 6 n/a +165.202917 1 motion 0 0 0 6 6 n/a +167.238008 1 motion 90 0 0 7 6 n/a +168.256216 1 motion 270 0 0 8 6 n/a +170.276472 1 motion 180 0 0 9 6 n/a +171.308784 1 motion 0 0 0 10 6 n/a +173.342917 1 motion 90 0 0 11 6 n/a +174.360172 1 motion 270 0 0 12 6 n/a +176.379105 1 motion 180 0 1 13 6 n/a +177.397767 1 motion 180 0 1 14 6 n/a +179.449094 1 motion 90 0 0 15 6 n/a +177.889151 0 response n/a n/a n/a n/a n/a b +180.468571 1 motion 270 0 0 16 6 n/a +189.077658 1 static -1 0 0 1 7 n/a +190.106728 1 static -2 0 0 2 7 n/a +192.141389 1 static -1 0 0 3 7 n/a +193.161252 1 static -2 0 0 4 7 n/a +195.191778 1 static -1 0 0 5 7 n/a +196.227832 1 static -2 0 0 6 7 n/a +198.24597 1 static -1 0 0 7 7 n/a +199.263611 1 static -2 0 0 8 7 n/a +201.28203 1 static -1 0 1 9 7 n/a +202.299353 1 static -1 0 1 10 7 n/a +204.332786 1 static -1 0 0 11 7 n/a +202.753173 0 response n/a n/a n/a n/a n/a b +205.369792 1 static -2 0 0 12 7 n/a +207.403127 1 static -1 0 1 13 7 n/a +208.421721 1 static -1 0 1 14 7 n/a +210.456094 1 static -1 0 0 15 7 n/a +209.441208 0 response n/a n/a n/a n/a n/a b +211.475322 1 static -2 0 0 16 7 n/a +219.148412 1 motion 180 0 0 1 8 n/a +220.180723 1 motion 0 0 0 2 8 n/a +222.214853 1 motion 90 0 0 3 8 n/a +223.234013 1 motion 270 0 0 4 8 n/a +225.251735 1 motion 180 0 0 5 8 n/a +226.285951 1 motion 0 0 0 6 8 n/a +228.320446 1 motion 90 0 1 7 8 n/a +229.337702 1 motion 90 0 1 8 8 n/a +231.355372 1 motion 180 0 0 9 8 n/a +230.105178 0 response n/a n/a n/a n/a n/a b +232.374351 1 motion 0 0 0 10 8 n/a +234.408528 1 motion 90 0 1 11 8 n/a +235.443879 1 motion 90 0 1 12 8 n/a +237.477097 1 motion 180 0 0 13 8 n/a +238.494171 1 motion 0 0 0 14 8 n/a +240.512438 1 motion 90 0 0 15 8 n/a +241.530646 1 motion 270 0 0 16 8 n/a +249.641977 1 static -1 0 0 1 9 n/a +250.672317 1 static -2 0 0 2 9 n/a +252.738665 1 static -1 0 1 3 9 n/a +253.759163 1 static -1 0 1 4 9 n/a +255.792599 1 static -1 0 0 5 9 n/a +254.185168 0 response n/a n/a n/a n/a n/a b +256.811193 1 static -2 0 0 6 9 n/a +258.844261 1 static -1 0 0 7 9 n/a +259.862854 1 static -2 0 0 8 9 n/a +261.89624 1 static -1 0 0 9 9 n/a +262.933563 1 static -2 0 0 10 9 n/a +264.965679 1 static -1 0 0 11 9 n/a +265.98459 1 static -2 0 0 12 9 n/a +268.003049 1 static -1 0 0 13 9 n/a +269.0203729999999 1 static -2 0 0 14 9 n/a +271.038211 1 static -1 0 0 15 9 n/a +272.073312 1 static -2 0 0 16 9 n/a +280.045129 1 motion 180 0 0 1 10 n/a +281.079664 1 motion 0 0 0 2 10 n/a +283.098234 1 motion 90 0 0 3 10 n/a +284.116124 1 motion 270 0 0 4 10 n/a +286.134167 1 motion 180 0 0 5 10 n/a +287.151876 1 motion 0 0 0 6 10 n/a +289.185375 1 motion 90 0 0 7 10 n/a +290.221996 1 motion 270 0 0 8 10 n/a +292.254271 1 motion 180 0 1 9 10 n/a +293.27198 1 motion 180 0 1 10 10 n/a +295.290606 1 motion 90 0 0 11 10 n/a +294.385153 0 response n/a n/a n/a n/a n/a b +296.309449 1 motion 270 0 0 12 10 n/a +298.327166 1 motion 180 0 0 13 10 n/a +299.395026 1 motion 0 0 0 14 10 n/a +301.413289 1 motion 90 0 0 15 10 n/a +302.43118 1 motion 270 0 0 16 10 n/a +311.01362 1 static -1 0 0 1 11 n/a +312.038879 1 static -2 0 0 2 11 n/a +314.078924 1 static -1 0 0 3 11 n/a +315.108311 1 static -2 0 0 4 11 n/a +317.1413879999999 1 static -1 0 0 5 11 n/a +318.159346 1 static -2 0 0 6 11 n/a +320.179356 1 static -1 0 1 7 11 n/a +321.19668 1 static -1 0 1 8 11 n/a +323.231059 1 static -1 0 0 9 11 n/a +321.905159 0 response n/a n/a n/a n/a n/a b +324.251241 1 static -2 0 0 10 11 n/a +326.281461 1 static -1 0 0 11 11 n/a +327.30196 1 static -2 0 0 12 11 n/a +329.335613 1 static -1 0 1 13 11 n/a +330.353889 1 static -1 0 1 14 11 n/a +332.371766 1 static -1 0 0 15 11 n/a +331.065163 0 response n/a n/a n/a n/a n/a b +333.391312 1 static -2 0 0 16 11 n/a +340.881249 1 motion 180 0 0 1 12 n/a +341.910703 1 motion 0 0 0 2 12 n/a +343.930546 1 motion 90 0 0 3 12 n/a +344.947802 1 motion 270 0 0 4 12 n/a +346.964886 1 motion 180 0 0 5 12 n/a +347.984501 1 motion 0 0 0 6 12 n/a +350.019282 1 motion 90 0 0 7 12 n/a +351.038124 1 motion 270 0 0 8 12 n/a +353.0714 1 motion 180 0 0 9 12 n/a +354.088474 1 motion 0 0 0 10 12 n/a +356.106144 1 motion 90 0 1 11 12 n/a +357.125304 1 motion 90 0 1 12 12 n/a +359.1439360000001 1 motion 180 0 0 13 12 n/a +357.665171 0 response n/a n/a n/a n/a n/a b +360.16228 1 motion 0 0 0 14 12 n/a +362.195773 1 motion 90 0 0 15 12 n/a +363.215251 1 motion 270 0 0 16 12 n/a +370.543444 1 static -1 0 0 1 13 n/a +371.570926 1 static -2 0 0 2 13 n/a +373.589656 1 static -1 0 1 3 13 n/a +374.623488 1 static -1 0 1 4 13 n/a +376.656285 1 static -1 0 0 5 13 n/a +377.67456 1 static -2 0 0 6 13 n/a +379.693669 1 static -1 0 0 7 13 n/a +380.710993 1 static -2 0 0 8 13 n/a +382.729726 1 static -1 0 1 9 13 n/a +383.74705 1 static -1 0 1 10 13 n/a +385.764932 1 static -1 0 0 11 13 n/a +384.385181 0 response n/a n/a n/a n/a n/a b +386.785112 1 static -2 0 0 12 13 n/a +388.816905 1 static -1 0 0 13 13 n/a +389.836133 1 static -2 0 0 14 13 n/a +391.86952 1 static -1 0 0 15 13 n/a +392.888114 1 static -2 0 0 16 13 n/a +399.978688 1 motion 180 0 0 1 14 n/a +401.011635 1 motion 0 0 0 2 14 n/a +403.042647 1 motion 90 0 1 3 14 n/a +404.062443 1 motion 90 0 1 4 14 n/a +406.079479 1 motion 180 0 0 5 14 n/a +404.673173 0 response n/a n/a n/a n/a n/a b +407.134331 1 motion 0 0 0 6 14 n/a +409.168141 1 motion 90 0 0 7 14 n/a +410.185397 1 motion 270 0 0 8 14 n/a +412.202163 1 motion 180 0 0 9 14 n/a +413.22019 1 motion 0 0 0 10 14 n/a +415.237861 1 motion 90 0 1 11 14 n/a +416.274164 1 motion 90 0 1 12 14 n/a +418.306758 1 motion 180 0 0 13 14 n/a +417.073168 0 response n/a n/a n/a n/a n/a b +419.326061 1 motion 0 0 0 14 14 n/a +421.345276 1 motion 90 0 0 15 14 n/a +422.36285 1 motion 270 0 0 16 14 n/a +430.548462 1 static -1 0 0 1 15 n/a +431.569913 1 static -2 0 0 2 15 n/a +433.603622 1 static -1 0 0 3 15 n/a +434.655549 1 static -2 0 0 4 15 n/a +436.67529 1 static -1 0 0 5 15 n/a +437.693559 1 static -2 0 0 6 15 n/a +439.725349 1 static -1 0 0 7 15 n/a +440.74172 1 static -2 0 0 8 15 n/a +442.760458 1 static -1 0 1 9 15 n/a +443.795242 1 static -1 0 1 10 15 n/a +445.813124 1 static -1 0 0 11 15 n/a +444.961179 0 response n/a n/a n/a n/a n/a b +446.834257 1 static -2 0 0 12 15 n/a +448.864146 1 static -1 0 0 13 15 n/a +449.885279 1 static -2 0 0 14 15 n/a +451.917087 1 static -1 0 0 15 15 n/a +452.954093 1 static -2 0 0 16 15 n/a +455.201185 0 response n/a n/a n/a n/a n/a b +460.854165 1 motion 180 0 0 1 16 n/a +461.875683 1 motion 0 0 0 2 16 n/a +463.911768 1 motion 90 0 1 3 16 n/a +464.930294 1 motion 90 0 1 4 16 n/a +466.963197 1 motion 180 0 0 5 16 n/a +465.529151 0 response n/a n/a n/a n/a n/a b +467.984398 1 motion 0 0 0 6 16 n/a +470.017271 1 motion 90 0 0 7 16 n/a +471.035797 1 motion 270 0 0 8 16 n/a +473.0541450000001 1 motion 180 0 0 9 16 n/a +474.072807 1 motion 0 0 0 10 16 n/a +476.105045 1 motion 90 0 0 11 16 n/a +477.122302 1 motion 270 0 0 12 16 n/a +479.141246 1 motion 180 0 1 13 16 n/a +480.15959 1 motion 180 0 1 14 16 n/a +482.193448 1 motion 90 0 0 15 16 n/a +481.249157 0 response n/a n/a n/a n/a n/a b +483.211973 1 motion 270 0 0 16 16 n/a +492.182983 1 static -1 0 0 1 17 n/a +493.204116 1 static -2 0 0 2 17 n/a +495.237815 1 static -1 0 0 3 17 n/a +496.2541859999999 1 static -2 0 0 4 17 n/a +498.27266 1 static -1 0 0 5 17 n/a +499.290937 1 static -2 0 0 6 17 n/a +501.309669 1 static -1 0 1 7 17 n/a +502.326675 1 static -1 0 1 8 17 n/a +504.345829 1 static -1 0 0 9 17 n/a +502.873151 0 response n/a n/a n/a n/a n/a b +505.365057 1 static -2 0 0 10 17 n/a +507.414306 1 static -1 0 0 11 17 n/a +508.431948 1 static -2 0 0 12 17 n/a +510.44973 1 static -1 0 1 13 17 n/a +511.466418 1 static -1 0 1 14 17 n/a +513.4852550000001 1 static -1 0 0 15 17 n/a +512.185184 0 response n/a n/a n/a n/a n/a b +514.505435 1 static -2 0 0 16 17 n/a +521.6299789999999 1 motion 180 0 0 1 18 n/a +522.660704 1 motion 0 0 0 2 18 n/a +524.695165 1 motion 90 0 0 3 18 n/a +525.7117860000001 1 motion 270 0 0 4 18 n/a +527.7288649999999 1 motion 180 0 0 5 18 n/a +528.748479 1 motion 0 0 0 6 18 n/a +530.7813560000001 1 motion 90 0 0 7 18 n/a +531.801786 1 motion 270 0 0 8 18 n/a +533.8191419999999 1 motion 180 0 1 9 18 n/a +534.8362159999999 1 motion 180 0 1 10 18 n/a +536.8710379999999 1 motion 90 0 0 11 18 n/a +535.361169 0 response n/a n/a n/a n/a n/a b +537.8898809999999 1 motion 270 0 0 12 18 n/a +539.9069589999999 1 motion 180 0 0 13 18 n/a +540.924033 1 motion 0 0 0 14 18 n/a +542.9429319999999 1 motion 90 0 0 15 18 n/a +543.96114 1 motion 270 0 0 16 18 n/a +552.683572 1 static -1 0 0 1 19 n/a +553.704387 1 static -2 0 0 2 19 n/a +555.7365110000001 1 static -1 0 0 3 19 n/a +556.772247 1 static -2 0 0 4 19 n/a +558.806586 1 static -1 0 0 5 19 n/a +559.823909 1 static -2 0 0 6 19 n/a +561.8423680000001 1 static -1 0 0 7 19 n/a +562.859375 1 static -2 0 0 8 19 n/a +564.894348 1 static -1 0 0 9 19 n/a +565.9116720000001 1 static -2 0 0 10 19 n/a +567.9304100000001 1 static -1 0 1 11 19 n/a +568.9493210000001 1 static -1 0 1 12 19 n/a +570.982759 1 static -1 0 0 13 19 n/a +569.257162 0 response n/a n/a n/a n/a n/a b +572.002306 1 static -2 0 0 14 19 n/a +574.0353720000001 1 static -1 0 0 15 19 n/a +575.052379 1 static -2 0 0 16 19 n/a +582.807382 1 motion 180 0 0 1 20 n/a +583.8257259999999 1 motion 0 0 0 2 20 n/a +585.844931 1 motion 90 0 0 3 20 n/a +586.8625039999999 1 motion 270 0 0 4 20 n/a +588.88086 1 motion 180 0 0 5 20 n/a +589.897935 1 motion 0 0 0 6 20 n/a +591.9168239999999 1 motion 90 0 0 7 20 n/a +592.934715 1 motion 270 0 0 8 20 n/a +594.9527009999999 1 motion 180 0 1 9 20 n/a +595.9691409999999 1 motion 180 0 1 10 20 n/a +598.0052299999999 1 motion 90 0 0 11 20 n/a +599.0234379999999 1 motion 270 0 0 12 20 n/a +601.056067 1 motion 180 0 0 13 20 n/a +602.091554 1 motion 0 0 0 14 20 n/a +604.1098169999999 1 motion 90 0 0 15 20 n/a +605.128032 1 motion 270 0 0 16 20 n/a diff --git a/tests/data/tsv_files/sub-01_task-TouchBefore_events.tsv b/tests/data/tsv_files/sub-01_task-TouchBefore_events.tsv new file mode 100644 index 00000000..aaac624f --- /dev/null +++ b/tests/data/tsv_files/sub-01_task-TouchBefore_events.tsv @@ -0,0 +1,349 @@ +onset duration trial_type direction soundTarget fixationTarget event block keyName +5.341079 0.250317 static -1.000000 0.000000 0.000000 1.000000 1.000000 n/a +5.624888 0.250181 static -2.000000 0.000000 0.000000 2.000000 1.000000 n/a +7.897202 0.250312 static -1.000000 0.000000 0.000000 3.000000 1.000000 n/a +8.197196 0.250175 static -2.000000 0.000000 0.000000 4.000000 1.000000 n/a +10.497456 0.250321 static -1.000000 0.000000 0.000000 5.000000 1.000000 n/a +10.805712 0.250167 static -2.000000 0.000000 0.000000 6.000000 1.000000 n/a +13.092630 0.250311 static -1.000000 0.000000 0.000000 7.000000 1.000000 n/a +13.379607 0.250182 static -2.000000 0.000000 0.000000 8.000000 1.000000 n/a +15.647173 0.250303 static -1.000000 0.000000 0.000000 9.000000 1.000000 n/a +15.932555 0.250176 static -2.000000 0.000000 0.000000 10.000000 1.000000 n/a +18.200748 0.250311 static -1.000000 0.000000 0.000000 11.000000 1.000000 n/a +18.468995 0.250175 static -2.000000 0.000000 0.000000 12.000000 1.000000 n/a +20.735601 0.250311 static -1.000000 0.000000 1.000000 13.000000 1.000000 n/a +21.003214 0.250311 static -1.000000 0.000000 1.000000 14.000000 1.000000 n/a +23.284436 0.250298 static -1.000000 0.000000 0.000000 15.000000 1.000000 n/a +22.998826 0.000000 response n/a n/a n/a n/a n/a b +23.576796 0.250175 static -2.000000 0.000000 0.000000 16.000000 1.000000 n/a +31.743250 0.250243 motion 180.000000 0.000000 0.000000 1.000000 2.000000 n/a +32.028640 0.250311 motion 0.000000 0.000000 0.000000 2.000000 2.000000 n/a +34.295247 0.250084 motion 90.000000 0.000000 0.000000 3.000000 2.000000 n/a +34.578732 0.250719 motion 270.000000 0.000000 0.000000 4.000000 2.000000 n/a +36.860895 0.250243 motion 180.000000 0.000000 0.000000 5.000000 2.000000 n/a +37.149142 0.250319 motion 0.000000 0.000000 0.000000 6.000000 2.000000 n/a +39.431622 0.250084 motion 90.000000 0.000000 1.000000 7.000000 2.000000 n/a +39.700187 0.250084 motion 90.000000 0.000000 1.000000 8.000000 2.000000 n/a +41.984572 0.250243 motion 180.000000 0.000000 0.000000 9.000000 2.000000 n/a +41.798833 0.000000 response n/a n/a n/a n/a n/a b +42.271866 0.250319 motion 0.000000 0.000000 0.000000 10.000000 2.000000 n/a +44.537845 0.250085 motion 90.000000 0.000000 0.000000 11.000000 2.000000 n/a +44.822600 0.250711 motion 270.000000 0.000000 0.000000 12.000000 2.000000 n/a +47.088883 0.250243 motion 180.000000 0.000000 1.000000 13.000000 2.000000 n/a +47.374589 0.250245 motion 180.000000 0.000000 1.000000 14.000000 2.000000 n/a +49.640566 0.250081 motion 90.000000 0.000000 0.000000 15.000000 2.000000 n/a +49.358849 0.000000 response n/a n/a n/a n/a n/a b +49.930079 0.250726 motion 270.000000 0.000000 0.000000 16.000000 2.000000 n/a +57.771774 0.250318 static -1.000000 0.000000 0.000000 1.000000 3.000000 n/a +58.047330 0.250175 static -2.000000 0.000000 0.000000 2.000000 3.000000 n/a +60.314563 0.250311 static -1.000000 0.000000 0.000000 3.000000 3.000000 n/a +60.601222 0.250175 static -2.000000 0.000000 0.000000 4.000000 3.000000 n/a +62.884337 0.250311 static -1.000000 0.000000 0.000000 5.000000 3.000000 n/a +63.170043 0.250177 static -2.000000 0.000000 0.000000 6.000000 3.000000 n/a +65.453800 0.250304 static -1.000000 0.000000 0.000000 7.000000 3.000000 n/a +65.739182 0.250175 static -2.000000 0.000000 0.000000 8.000000 3.000000 n/a +68.005791 0.250308 static -1.000000 0.000000 1.000000 9.000000 3.000000 n/a +68.289273 0.250310 static -1.000000 0.000000 1.000000 10.000000 3.000000 n/a +70.573021 0.250311 static -1.000000 0.000000 0.000000 11.000000 3.000000 n/a +70.860005 0.250175 static -2.000000 0.000000 0.000000 12.000000 3.000000 n/a +73.126292 0.250303 static -1.000000 0.000000 1.000000 13.000000 3.000000 n/a +73.038847 0.000000 response n/a n/a n/a n/a n/a b +73.413579 0.250311 static -1.000000 0.000000 1.000000 14.000000 3.000000 n/a +75.680196 0.250311 static -1.000000 0.000000 0.000000 15.000000 3.000000 n/a +75.014831 0.000000 response n/a n/a n/a n/a n/a b +75.953522 0.250162 static -2.000000 0.000000 0.000000 16.000000 3.000000 n/a +84.725019 0.250243 motion 180.000000 0.000000 0.000000 1.000000 4.000000 n/a +85.021201 0.250311 motion 0.000000 0.000000 0.000000 2.000000 4.000000 n/a +87.303996 0.250085 motion 90.000000 0.000000 0.000000 3.000000 4.000000 n/a +87.590022 0.250717 motion 270.000000 0.000000 0.000000 4.000000 4.000000 n/a +89.872814 0.250243 motion 180.000000 0.000000 0.000000 5.000000 4.000000 n/a +90.159473 0.250318 motion 0.000000 0.000000 0.000000 6.000000 4.000000 n/a +92.425443 0.250084 motion 90.000000 0.000000 0.000000 7.000000 4.000000 n/a +92.709245 0.250719 motion 270.000000 0.000000 0.000000 8.000000 4.000000 n/a +94.992991 0.250242 motion 180.000000 0.000000 1.000000 9.000000 4.000000 n/a +95.279650 0.250250 motion 180.000000 0.000000 1.000000 10.000000 4.000000 n/a +97.545938 0.250084 motion 90.000000 0.000000 0.000000 11.000000 4.000000 n/a +97.150840 0.000000 response n/a n/a n/a n/a n/a b +97.833549 0.250726 motion 270.000000 0.000000 0.000000 12.000000 4.000000 n/a +100.101422 0.250242 motion 180.000000 0.000000 0.000000 13.000000 4.000000 n/a +100.369034 0.250311 motion 0.000000 0.000000 0.000000 14.000000 4.000000 n/a +102.635956 0.250091 motion 90.000000 0.000000 0.000000 15.000000 4.000000 n/a +102.903893 0.250712 motion 270.000000 0.000000 0.000000 16.000000 4.000000 n/a +111.869036 0.250310 static -1.000000 0.000000 0.000000 1.000000 5.000000 n/a +112.159821 0.250174 static -2.000000 0.000000 0.000000 2.000000 5.000000 n/a +114.442932 0.250310 static -1.000000 0.000000 0.000000 3.000000 5.000000 n/a +114.727686 0.250174 static -2.000000 0.000000 0.000000 4.000000 5.000000 n/a +117.011750 0.250310 static -1.000000 0.000000 0.000000 5.000000 5.000000 n/a +117.298411 0.250175 static -2.000000 0.000000 0.000000 6.000000 5.000000 n/a +119.565647 0.250310 static -1.000000 0.000000 1.000000 7.000000 5.000000 n/a +119.848497 0.250311 static -1.000000 0.000000 1.000000 8.000000 5.000000 n/a +122.115418 0.250311 static -1.000000 0.000000 0.000000 9.000000 5.000000 n/a +121.614782 0.000000 response n/a n/a n/a n/a n/a b +122.402712 0.250176 static -2.000000 0.000000 0.000000 10.000000 5.000000 n/a +124.669958 0.250303 static -1.000000 0.000000 0.000000 11.000000 5.000000 n/a +124.953752 0.250174 static -2.000000 0.000000 0.000000 12.000000 5.000000 n/a +127.236864 0.250310 static -1.000000 0.000000 0.000000 13.000000 5.000000 n/a +127.522887 0.250177 static -2.000000 0.000000 0.000000 14.000000 5.000000 n/a +129.788857 0.250310 static -1.000000 0.000000 0.000000 15.000000 5.000000 n/a +130.073293 0.250182 static -2.000000 0.000000 0.000000 16.000000 5.000000 n/a +137.383252 0.250243 motion 180.000000 0.000000 0.000000 1.000000 6.000000 n/a +137.652134 0.250310 motion 0.000000 0.000000 0.000000 2.000000 6.000000 n/a +139.946673 0.250084 motion 90.000000 0.000000 1.000000 3.000000 6.000000 n/a +140.233967 0.250084 motion 90.000000 0.000000 1.000000 4.000000 6.000000 n/a +142.525967 0.250250 motion 180.000000 0.000000 0.000000 5.000000 6.000000 n/a +142.817395 0.250304 motion 0.000000 0.000000 0.000000 6.000000 6.000000 n/a +145.100500 0.250084 motion 90.000000 0.000000 0.000000 7.000000 6.000000 n/a +145.370023 0.250719 motion 270.000000 0.000000 0.000000 8.000000 6.000000 n/a +147.651223 0.250250 motion 180.000000 0.000000 0.000000 9.000000 6.000000 n/a +147.939158 0.250303 motion 0.000000 0.000000 0.000000 10.000000 6.000000 n/a +150.222897 0.250084 motion 90.000000 0.000000 0.000000 11.000000 6.000000 n/a +150.508603 0.250719 motion 270.000000 0.000000 0.000000 12.000000 6.000000 n/a +152.776167 0.250235 motion 180.000000 0.000000 0.000000 13.000000 6.000000 n/a +153.059644 0.250310 motion 0.000000 0.000000 0.000000 14.000000 6.000000 n/a +155.342127 0.250077 motion 90.000000 0.000000 0.000000 15.000000 6.000000 n/a +155.628144 0.250719 motion 270.000000 0.000000 0.000000 16.000000 6.000000 n/a +164.491073 0.250310 static -1.000000 0.000000 0.000000 1.000000 7.000000 n/a +164.787573 0.250174 static -2.000000 0.000000 0.000000 2.000000 7.000000 n/a +167.053859 0.250316 static -1.000000 0.000000 0.000000 3.000000 7.000000 n/a +167.336714 0.250168 static -2.000000 0.000000 0.000000 4.000000 7.000000 n/a +169.621088 0.250311 static -1.000000 0.000000 1.000000 5.000000 7.000000 n/a +169.907430 0.250310 static -1.000000 0.000000 1.000000 6.000000 7.000000 n/a +172.174350 0.250310 static -1.000000 0.000000 0.000000 7.000000 7.000000 n/a +171.598851 0.000000 response n/a n/a n/a n/a n/a b +172.461326 0.250174 static -2.000000 0.000000 0.000000 8.000000 7.000000 n/a +174.758406 0.250303 static -1.000000 0.000000 1.000000 9.000000 7.000000 n/a +175.027597 0.250310 static -1.000000 0.000000 1.000000 10.000000 7.000000 n/a +177.310386 0.250310 static -1.000000 0.000000 0.000000 11.000000 7.000000 n/a +176.766841 0.000000 response n/a n/a n/a n/a n/a b +177.598957 0.250174 static -2.000000 0.000000 0.000000 12.000000 7.000000 n/a +179.865868 0.250313 static -1.000000 0.000000 0.000000 13.000000 7.000000 n/a +180.134117 0.250172 static -2.000000 0.000000 0.000000 14.000000 7.000000 n/a +182.400717 0.250310 static -1.000000 0.000000 0.000000 15.000000 7.000000 n/a +182.668646 0.250175 static -2.000000 0.000000 0.000000 16.000000 7.000000 n/a +190.577629 0.250243 motion 180.000000 0.000000 0.000000 1.000000 8.000000 n/a +190.874446 0.250317 motion 0.000000 0.000000 0.000000 2.000000 8.000000 n/a +193.140098 0.250084 motion 90.000000 0.000000 1.000000 3.000000 8.000000 n/a +193.425804 0.250091 motion 90.000000 0.000000 1.000000 4.000000 8.000000 n/a +195.692091 0.250242 motion 180.000000 0.000000 0.000000 5.000000 8.000000 n/a +195.962242 0.250310 motion 0.000000 0.000000 0.000000 6.000000 8.000000 n/a +198.228211 0.250086 motion 90.000000 0.000000 0.000000 7.000000 8.000000 n/a +196.566855 0.000000 response n/a n/a n/a n/a n/a b +198.514873 0.250717 motion 270.000000 0.000000 0.000000 8.000000 8.000000 n/a +200.783062 0.250243 motion 180.000000 0.000000 1.000000 9.000000 8.000000 n/a +201.050675 0.250242 motion 180.000000 0.000000 1.000000 10.000000 8.000000 n/a +203.317910 0.250084 motion 90.000000 0.000000 0.000000 11.000000 8.000000 n/a +203.134857 0.000000 response n/a n/a n/a n/a n/a b +203.587108 0.250726 motion 270.000000 0.000000 0.000000 12.000000 8.000000 n/a +205.883232 0.250242 motion 180.000000 0.000000 0.000000 13.000000 8.000000 n/a +206.169891 0.250310 motion 0.000000 0.000000 0.000000 14.000000 8.000000 n/a +208.453318 0.250084 motion 90.000000 0.000000 0.000000 15.000000 8.000000 n/a +208.738390 0.250718 motion 270.000000 0.000000 0.000000 16.000000 8.000000 n/a +217.096884 0.250318 static -1.000000 0.000000 0.000000 1.000000 9.000000 n/a +217.364503 0.250167 static -2.000000 0.000000 0.000000 2.000000 9.000000 n/a +219.630465 0.250310 static -1.000000 0.000000 0.000000 3.000000 9.000000 n/a +219.915219 0.250174 static -2.000000 0.000000 0.000000 4.000000 9.000000 n/a +222.182140 0.250311 static -1.000000 0.000000 1.000000 5.000000 9.000000 n/a +222.466577 0.250317 static -1.000000 0.000000 1.000000 6.000000 9.000000 n/a +224.733822 0.250303 static -1.000000 0.000000 0.000000 7.000000 9.000000 n/a +224.582839 0.000000 response n/a n/a n/a n/a n/a b +225.020474 0.250176 static -2.000000 0.000000 0.000000 8.000000 9.000000 n/a +227.286761 0.250318 static -1.000000 0.000000 0.000000 9.000000 9.000000 n/a +227.571522 0.250175 static -2.000000 0.000000 0.000000 10.000000 9.000000 n/a +229.885738 0.250311 static -1.000000 0.000000 1.000000 11.000000 9.000000 n/a +230.172714 0.250311 static -1.000000 0.000000 1.000000 12.000000 9.000000 n/a +232.456462 0.250311 static -1.000000 0.000000 0.000000 13.000000 9.000000 n/a +232.166829 0.000000 response n/a n/a n/a n/a n/a b +232.741216 0.250174 static -2.000000 0.000000 0.000000 14.000000 9.000000 n/a +235.024327 0.250311 static -1.000000 0.000000 0.000000 15.000000 9.000000 n/a +235.310034 0.250174 static -2.000000 0.000000 0.000000 16.000000 9.000000 n/a +243.530770 0.250243 motion 180.000000 0.000000 0.000000 1.000000 10.000000 n/a +243.800286 0.250303 motion 0.000000 0.000000 0.000000 2.000000 10.000000 n/a +246.084028 0.250083 motion 90.000000 0.000000 1.000000 3.000000 10.000000 n/a +246.368781 0.250091 motion 90.000000 0.000000 1.000000 4.000000 10.000000 n/a +248.650314 0.250235 motion 180.000000 0.000000 0.000000 5.000000 10.000000 n/a +248.758836 0.000000 response n/a n/a n/a n/a n/a b +248.940140 0.250311 motion 0.000000 0.000000 0.000000 6.000000 10.000000 n/a +251.205475 0.250084 motion 90.000000 0.000000 0.000000 7.000000 10.000000 n/a +251.490864 0.250719 motion 270.000000 0.000000 0.000000 8.000000 10.000000 n/a +253.757158 0.250242 motion 180.000000 0.000000 0.000000 9.000000 10.000000 n/a +254.043182 0.250304 motion 0.000000 0.000000 0.000000 10.000000 10.000000 n/a +256.310732 0.250084 motion 90.000000 0.000000 0.000000 11.000000 10.000000 n/a +256.595803 0.250726 motion 270.000000 0.000000 0.000000 12.000000 10.000000 n/a +258.863366 0.250242 motion 180.000000 0.000000 0.000000 13.000000 10.000000 n/a +259.130978 0.250311 motion 0.000000 0.000000 0.000000 14.000000 10.000000 n/a +261.398210 0.250084 motion 90.000000 0.000000 0.000000 15.000000 10.000000 n/a +261.680424 0.250719 motion 270.000000 0.000000 0.000000 16.000000 10.000000 n/a +270.528117 0.250310 static -1.000000 0.000000 0.000000 1.000000 11.000000 n/a +270.822394 0.250178 static -2.000000 0.000000 0.000000 2.000000 11.000000 n/a +273.105188 0.250314 static -1.000000 0.000000 0.000000 3.000000 11.000000 n/a +273.389628 0.250175 static -2.000000 0.000000 0.000000 4.000000 11.000000 n/a +275.673372 0.250318 static -1.000000 0.000000 1.000000 5.000000 11.000000 n/a +275.943848 0.250304 static -1.000000 0.000000 1.000000 6.000000 11.000000 n/a +278.211080 0.250310 static -1.000000 0.000000 0.000000 7.000000 11.000000 n/a +277.622858 0.000000 response n/a n/a n/a n/a n/a b +278.497421 0.250174 static -2.000000 0.000000 0.000000 8.000000 11.000000 n/a +280.763391 0.250311 static -1.000000 0.000000 1.000000 9.000000 11.000000 n/a +281.047827 0.250313 static -1.000000 0.000000 1.000000 10.000000 11.000000 n/a +283.330939 0.250311 static -1.000000 0.000000 0.000000 11.000000 11.000000 n/a +283.054847 0.000000 response n/a n/a n/a n/a n/a b +283.618233 0.250175 static -2.000000 0.000000 0.000000 12.000000 11.000000 n/a +285.885796 0.250303 static -1.000000 0.000000 0.000000 13.000000 11.000000 n/a +286.153401 0.250174 static -2.000000 0.000000 0.000000 14.000000 11.000000 n/a +288.420640 0.250311 static -1.000000 0.000000 0.000000 15.000000 11.000000 n/a +288.687934 0.250175 static -2.000000 0.000000 0.000000 16.000000 11.000000 n/a +296.412800 0.250250 motion 180.000000 0.000000 0.000000 1.000000 12.000000 n/a +296.710260 0.250303 motion 0.000000 0.000000 0.000000 2.000000 12.000000 n/a +298.977808 0.250084 motion 90.000000 0.000000 0.000000 3.000000 12.000000 n/a +299.262879 0.250726 motion 270.000000 0.000000 0.000000 4.000000 12.000000 n/a +301.530435 0.250250 motion 180.000000 0.000000 0.000000 5.000000 12.000000 n/a +301.814879 0.250304 motion 0.000000 0.000000 0.000000 6.000000 12.000000 n/a +304.097032 0.250082 motion 90.000000 0.000000 1.000000 7.000000 12.000000 n/a +304.383371 0.250091 motion 90.000000 0.000000 1.000000 8.000000 12.000000 n/a +306.664894 0.250242 motion 180.000000 0.000000 0.000000 9.000000 12.000000 n/a +306.430797 0.000000 response n/a n/a n/a n/a n/a b +306.952823 0.250310 motion 0.000000 0.000000 0.000000 10.000000 12.000000 n/a +309.219751 0.250077 motion 90.000000 0.000000 0.000000 11.000000 12.000000 n/a +309.504180 0.250719 motion 270.000000 0.000000 0.000000 12.000000 12.000000 n/a +311.771108 0.250243 motion 180.000000 0.000000 0.000000 13.000000 12.000000 n/a +312.056180 0.250305 motion 0.000000 0.000000 0.000000 14.000000 12.000000 n/a +314.322459 0.250084 motion 90.000000 0.000000 0.000000 15.000000 12.000000 n/a +314.608483 0.250719 motion 270.000000 0.000000 0.000000 16.000000 12.000000 n/a +322.186052 0.250310 static -1.000000 0.000000 0.000000 1.000000 13.000000 n/a +322.480965 0.250176 static -2.000000 0.000000 0.000000 2.000000 13.000000 n/a +324.764076 0.250311 static -1.000000 0.000000 0.000000 3.000000 13.000000 n/a +325.050101 0.250175 static -2.000000 0.000000 0.000000 4.000000 13.000000 n/a +327.332895 0.250310 static -1.000000 0.000000 1.000000 5.000000 13.000000 n/a +327.618284 0.250310 static -1.000000 0.000000 1.000000 6.000000 13.000000 n/a +329.900763 0.250308 static -1.000000 0.000000 0.000000 7.000000 13.000000 n/a +329.102840 0.000000 response n/a n/a n/a n/a n/a b +330.189641 0.250174 static -2.000000 0.000000 0.000000 8.000000 13.000000 n/a +332.456564 0.250311 static -1.000000 0.000000 0.000000 9.000000 13.000000 n/a +332.725128 0.250174 static -2.000000 0.000000 0.000000 10.000000 13.000000 n/a +335.023485 0.250311 static -1.000000 0.000000 1.000000 11.000000 13.000000 n/a +335.307287 0.250303 static -1.000000 0.000000 1.000000 12.000000 13.000000 n/a +337.574519 0.250311 static -1.000000 0.000000 0.000000 13.000000 13.000000 n/a +337.222840 0.000000 response n/a n/a n/a n/a n/a b +337.862448 0.250174 static -2.000000 0.000000 0.000000 14.000000 13.000000 n/a +340.129053 0.250311 static -1.000000 0.000000 0.000000 15.000000 13.000000 n/a +340.396982 0.250175 static -2.000000 0.000000 0.000000 16.000000 13.000000 n/a +347.737107 0.250236 motion 180.000000 0.000000 0.000000 1.000000 14.000000 n/a +348.005664 0.250312 motion 0.000000 0.000000 0.000000 2.000000 14.000000 n/a +350.299263 0.250084 motion 90.000000 0.000000 1.000000 3.000000 14.000000 n/a +350.587504 0.250084 motion 90.000000 0.000000 1.000000 4.000000 14.000000 n/a +352.853783 0.250243 motion 180.000000 0.000000 0.000000 5.000000 14.000000 n/a +352.934815 0.000000 response n/a n/a n/a n/a n/a b +353.141077 0.250310 motion 0.000000 0.000000 0.000000 6.000000 14.000000 n/a +355.407363 0.250084 motion 90.000000 0.000000 1.000000 7.000000 14.000000 n/a +355.691800 0.250084 motion 90.000000 0.000000 1.000000 8.000000 14.000000 n/a +357.958409 0.250239 motion 180.000000 0.000000 0.000000 9.000000 14.000000 n/a +358.242842 0.250311 motion 0.000000 0.000000 0.000000 10.000000 14.000000 n/a +360.510079 0.250087 motion 90.000000 0.000000 0.000000 11.000000 14.000000 n/a +360.795471 0.250716 motion 270.000000 0.000000 0.000000 12.000000 14.000000 n/a +363.078263 0.250242 motion 180.000000 0.000000 0.000000 13.000000 14.000000 n/a +363.364604 0.250311 motion 0.000000 0.000000 0.000000 14.000000 14.000000 n/a +365.645811 0.250095 motion 90.000000 0.000000 0.000000 15.000000 14.000000 n/a +365.933751 0.250710 motion 270.000000 0.000000 0.000000 16.000000 14.000000 n/a +374.351919 0.250311 static -1.000000 0.000000 0.000000 1.000000 15.000000 n/a +374.656038 0.250174 static -2.000000 0.000000 0.000000 2.000000 15.000000 n/a +376.939784 0.250310 static -1.000000 0.000000 0.000000 3.000000 15.000000 n/a +377.207396 0.250177 static -2.000000 0.000000 0.000000 4.000000 15.000000 n/a +379.491467 0.250311 static -1.000000 0.000000 0.000000 5.000000 15.000000 n/a +379.778443 0.250175 static -2.000000 0.000000 0.000000 6.000000 15.000000 n/a +382.045040 0.250312 static -1.000000 0.000000 0.000000 7.000000 15.000000 n/a +382.328843 0.250173 static -2.000000 0.000000 0.000000 8.000000 15.000000 n/a +384.612909 0.250311 static -1.000000 0.000000 0.000000 9.000000 15.000000 n/a +384.898937 0.250175 static -2.000000 0.000000 0.000000 10.000000 15.000000 n/a +387.165535 0.250310 static -1.000000 0.000000 0.000000 11.000000 15.000000 n/a +387.451241 0.250182 static -2.000000 0.000000 0.000000 12.000000 15.000000 n/a +389.717845 0.250313 static -1.000000 0.000000 1.000000 13.000000 15.000000 n/a +390.001332 0.250309 static -1.000000 0.000000 1.000000 14.000000 15.000000 n/a +392.285089 0.250302 static -1.000000 0.000000 0.000000 15.000000 15.000000 n/a +391.918802 0.000000 response n/a n/a n/a n/a n/a b +392.575549 0.250175 static -2.000000 0.000000 0.000000 16.000000 15.000000 n/a +400.707711 0.250239 motion 180.000000 0.000000 0.000000 1.000000 16.000000 n/a +400.994367 0.250311 motion 0.000000 0.000000 0.000000 2.000000 16.000000 n/a +403.277797 0.250084 motion 90.000000 0.000000 0.000000 3.000000 16.000000 n/a +403.562551 0.250721 motion 270.000000 0.000000 0.000000 4.000000 16.000000 n/a +405.847251 0.250243 motion 180.000000 0.000000 0.000000 5.000000 16.000000 n/a +406.132323 0.250311 motion 0.000000 0.000000 0.000000 6.000000 16.000000 n/a +408.416072 0.250084 motion 90.000000 0.000000 1.000000 7.000000 16.000000 n/a +408.701460 0.250082 motion 90.000000 0.000000 1.000000 8.000000 16.000000 n/a +410.967748 0.250243 motion 180.000000 0.000000 0.000000 9.000000 16.000000 n/a +410.782851 0.000000 response n/a n/a n/a n/a n/a b +411.255674 0.250311 motion 0.000000 0.000000 0.000000 10.000000 16.000000 n/a +413.551801 0.250083 motion 90.000000 0.000000 1.000000 11.000000 16.000000 n/a +413.839729 0.250091 motion 90.000000 0.000000 1.000000 12.000000 16.000000 n/a +416.107602 0.250242 motion 180.000000 0.000000 0.000000 13.000000 16.000000 n/a +415.870779 0.000000 response n/a n/a n/a n/a n/a b +416.376166 0.250311 motion 0.000000 0.000000 0.000000 14.000000 16.000000 n/a +418.643090 0.250092 motion 90.000000 0.000000 0.000000 15.000000 16.000000 n/a +418.910392 0.250719 motion 270.000000 0.000000 0.000000 16.000000 16.000000 n/a +428.130127 0.250311 static -1.000000 0.000000 0.000000 1.000000 17.000000 n/a +428.418056 0.250182 static -2.000000 0.000000 0.000000 2.000000 17.000000 n/a +430.684977 0.250310 static -1.000000 0.000000 0.000000 3.000000 17.000000 n/a +430.969731 0.250175 static -2.000000 0.000000 0.000000 4.000000 17.000000 n/a +433.235700 0.250318 static -1.000000 0.000000 0.000000 5.000000 17.000000 n/a +433.520144 0.250175 static -2.000000 0.000000 0.000000 6.000000 17.000000 n/a +435.803883 0.250310 static -1.000000 0.000000 1.000000 7.000000 17.000000 n/a +436.090859 0.250311 static -1.000000 0.000000 1.000000 8.000000 17.000000 n/a +438.385399 0.250311 static -1.000000 0.000000 0.000000 9.000000 17.000000 n/a +437.678850 0.000000 response n/a n/a n/a n/a n/a b +438.654280 0.250174 static -2.000000 0.000000 0.000000 10.000000 17.000000 n/a +440.941519 0.250311 static -1.000000 0.000000 0.000000 11.000000 17.000000 n/a +441.228496 0.250174 static -2.000000 0.000000 0.000000 12.000000 17.000000 n/a +443.495100 0.250311 static -1.000000 0.000000 0.000000 13.000000 17.000000 n/a +443.780171 0.250185 static -2.000000 0.000000 0.000000 14.000000 17.000000 n/a +446.047418 0.250305 static -1.000000 0.000000 0.000000 15.000000 17.000000 n/a +446.331532 0.250175 static -2.000000 0.000000 0.000000 16.000000 17.000000 n/a +453.722126 0.250241 motion 180.000000 0.000000 0.000000 1.000000 18.000000 n/a +453.989736 0.250313 motion 0.000000 0.000000 0.000000 2.000000 18.000000 n/a +456.255706 0.250084 motion 90.000000 0.000000 1.000000 3.000000 18.000000 n/a +456.540460 0.250084 motion 90.000000 0.000000 1.000000 4.000000 18.000000 n/a +458.822619 0.250243 motion 180.000000 0.000000 0.000000 5.000000 18.000000 n/a +458.670837 0.000000 response n/a n/a n/a n/a n/a b +459.110866 0.250311 motion 0.000000 0.000000 0.000000 6.000000 18.000000 n/a +461.391124 0.250082 motion 90.000000 0.000000 1.000000 7.000000 18.000000 n/a +461.680002 0.250084 motion 90.000000 0.000000 1.000000 8.000000 18.000000 n/a +463.947242 0.250243 motion 180.000000 0.000000 0.000000 9.000000 18.000000 n/a +463.542849 0.000000 response n/a n/a n/a n/a n/a b +464.216441 0.250310 motion 0.000000 0.000000 0.000000 10.000000 18.000000 n/a +466.512887 0.250084 motion 90.000000 0.000000 0.000000 11.000000 18.000000 n/a +466.798593 0.250719 motion 270.000000 0.000000 0.000000 12.000000 18.000000 n/a +469.081392 0.250241 motion 180.000000 0.000000 0.000000 13.000000 18.000000 n/a +469.365827 0.250307 motion 0.000000 0.000000 0.000000 14.000000 18.000000 n/a +471.650527 0.250084 motion 90.000000 0.000000 0.000000 15.000000 18.000000 n/a +471.936231 0.250719 motion 270.000000 0.000000 0.000000 16.000000 18.000000 n/a +480.923923 0.250311 static -1.000000 0.000000 0.000000 1.000000 19.000000 n/a +481.211217 0.250175 static -2.000000 0.000000 0.000000 2.000000 19.000000 n/a +483.477189 0.250308 static -1.000000 0.000000 0.000000 3.000000 19.000000 n/a +483.762576 0.250175 static -2.000000 0.000000 0.000000 4.000000 19.000000 n/a +486.044736 0.250311 static -1.000000 0.000000 0.000000 5.000000 19.000000 n/a +486.329807 0.250182 static -2.000000 0.000000 0.000000 6.000000 19.000000 n/a +488.613555 0.250311 static -1.000000 0.000000 1.000000 7.000000 19.000000 n/a +488.900531 0.250311 static -1.000000 0.000000 1.000000 8.000000 19.000000 n/a +491.166819 0.250311 static -1.000000 0.000000 0.000000 9.000000 19.000000 n/a +490.846817 0.000000 response n/a n/a n/a n/a n/a b +491.452843 0.250174 static -2.000000 0.000000 0.000000 10.000000 19.000000 n/a +493.719447 0.250318 static -1.000000 0.000000 0.000000 11.000000 19.000000 n/a +494.004844 0.250167 static -2.000000 0.000000 0.000000 12.000000 19.000000 n/a +496.271123 0.250310 static -1.000000 0.000000 0.000000 13.000000 19.000000 n/a +496.554607 0.250175 static -2.000000 0.000000 0.000000 14.000000 19.000000 n/a +498.838990 0.250309 static -1.000000 0.000000 0.000000 15.000000 19.000000 n/a +499.125330 0.250175 static -2.000000 0.000000 0.000000 16.000000 19.000000 n/a +507.129556 0.250243 motion 180.000000 0.000000 0.000000 1.000000 20.000000 n/a +507.415580 0.250317 motion 0.000000 0.000000 0.000000 2.000000 20.000000 n/a +509.714889 0.250084 motion 90.000000 0.000000 0.000000 3.000000 20.000000 n/a +510.001230 0.250711 motion 270.000000 0.000000 0.000000 4.000000 20.000000 n/a +512.268779 0.250243 motion 180.000000 0.000000 0.000000 5.000000 20.000000 n/a +512.552581 0.250311 motion 0.000000 0.000000 0.000000 6.000000 20.000000 n/a +514.833471 0.250084 motion 90.000000 0.000000 0.000000 7.000000 20.000000 n/a +515.122034 0.250719 motion 270.000000 0.000000 0.000000 8.000000 20.000000 n/a +517.388320 0.250242 motion 180.000000 0.000000 0.000000 9.000000 20.000000 n/a +517.672757 0.250311 motion 0.000000 0.000000 0.000000 10.000000 20.000000 n/a +519.956186 0.250086 motion 90.000000 0.000000 1.000000 11.000000 20.000000 n/a +520.242846 0.250082 motion 90.000000 0.000000 1.000000 12.000000 20.000000 n/a +522.509766 0.250243 motion 180.000000 0.000000 0.000000 13.000000 20.000000 n/a +522.318836 0.000000 response n/a n/a n/a n/a n/a b +522.795790 0.250318 motion 0.000000 0.000000 0.000000 14.000000 20.000000 n/a +525.063347 0.250084 motion 90.000000 0.000000 0.000000 15.000000 20.000000 n/a +525.330641 0.250719 motion 270.000000 0.000000 0.000000 16.000000 20.000000 n/a diff --git a/tests/miss_hit.cfg b/tests/miss_hit.cfg index 7f7c1dcf..0e88dec8 100644 --- a/tests/miss_hit.cfg +++ b/tests/miss_hit.cfg @@ -3,3 +3,5 @@ exclude_dir: "bids-examples" suppress_rule: "copyright_notice" + +metric "file_length": limit 900 diff --git a/tests/test_bids_file.m b/tests/test_bids_file.m index 953fb3e3..44cbb091 100644 --- a/tests/test_bids_file.m +++ b/tests/test_bids_file.m @@ -15,6 +15,162 @@ % % end +function test_no_entity_warning + + if bids.internal.is_octave + return + end + + assertWarning(@()bids.File('TStatistic.nii', 'verbose', true), ... + 'File:noEntity'); + +end + +function test_participants + + pth_bids_example = get_test_data_dir(); + + file = fullfile(pth_bids_example, 'pet002', 'participants.tsv'); + + bf = bids.File(file); + +end + +function test_get_metadata_suffixes_basic() + % ensures that "similar" suffixes are distinguished + + data_dir = fullfile(fileparts(mfilename('fullpath')), 'data', 'surface_data'); + + file = fullfile(data_dir, 'sub-06_hemi-R_space-individual_den-native_thickness.shape.gii'); + side_car = fullfile(data_dir, 'sub-06_hemi-R_space-individual_den-native_thickness.json'); + + bf = bids.File(file); + + % TODO only only json file per folder level allowed + % assertEqual(numel(bf.metadata_files), 1) + + expected_metadata = bids.util.jsondecode(side_car); + + assertEqual(bf.metadata, expected_metadata); + + file = fullfile(data_dir, 'sub-06_hemi-R_space-individual_den-native_midthickness.surf.gii'); + side_car = fullfile(data_dir, 'sub-06_hemi-R_space-individual_den-native_midthickness.json'); + + bf = bids.File(file); + + expected_metadata = bids.util.jsondecode(side_car); + + assertEqual(bf.metadata, expected_metadata); + + file = fullfile(data_dir, 'sub-06_space-individual_den-native_thickness.dscalar.nii'); + side_car = fullfile(data_dir, 'sub-06_space-individual_den-native_thickness.json'); + + bf = bids.File(file); + + expected_metadata = bids.util.jsondecode(side_car); + + assertEqual(bf.metadata, expected_metadata); + +end + +function test_rename() + + input_filename = 'wuasub-01_ses-test_task-faceRecognition_run-02_bold.nii'; + input_file = fullfile(fileparts(mfilename('fullpath')), input_filename); + output_filename = 'sub-01_ses-test_task-faceRecognition_run-02_desc-preproc_bold.nii'; + output_file = fullfile(fileparts(mfilename('fullpath')), output_filename); + + set_up(input_file); + teardown(output_file); + + file = bids.File(input_file, 'use_schema', false, 'verbose', false); + + assertEqual(file.path, input_file); + + file.prefix = ''; + file.entities.desc = 'preproc'; + assertEqual(file.filename, output_filename); + + file.rename(); + assertEqual(exist(output_file, 'file'), 0); + + file.rename('dry_run', true); + assertEqual(exist(output_file, 'file'), 0); + + file = file.rename('dry_run', false); + assertEqual(exist(input_file, 'file'), 0); + assertEqual(exist(output_file, 'file'), 2); + assertEqual(file.path, output_file); + + teardown(output_file); + +end + +function test_rename_with_spec() + + input_filename = 'wuasub-01_task-faceRecognition_bold.nii'; + output_filename = 'sub-01_task-faceRecognition_label-GM_desc-bold_dseg.json'; + file = bids.File(input_filename, 'use_schema', false); + + spec.prefix = ''; + spec.entities.desc = 'bold'; + spec.entities.label = 'GM'; + spec.suffix = 'dseg'; + spec.ext = '.json'; + spec.entity_order = {'sub', 'task', 'label', 'desc'}; + + file = file.rename('spec', spec); + assertEqual(file.filename, output_filename); + +end + +function test_rename_force() + + input_filename = 'wuasub-01_ses-test_task-faceRecognition_run-02_bold.nii'; + input_file = fullfile(fileparts(mfilename('fullpath')), input_filename); + output_filename = 'sub-01_ses-test_task-faceRecognition_run-02_desc-preproc_bold.nii'; + output_file = fullfile(fileparts(mfilename('fullpath')), output_filename); + + set_up(input_file); + set_up(output_file); + + system(sprintf('touch %s', input_file)); + system(sprintf('touch %s', output_file)); + file = bids.File(input_file, 'use_schema', false, 'verbose', false); + + assertEqual(file.path, input_file); + + file.prefix = ''; + file.entities.desc = 'preproc'; + file.verbose = true; + if bids.internal.is_github_ci && ~bids.internal.is_octave + % failure: warning 'Octave:mixed-string-concat' was raised, expected 'File:fileAlreadyExists'. + assertWarning(@() file.rename('dry_run', false), 'File:fileAlreadyExists'); + end + + file = file.rename('dry_run', false, 'verbose', false); + assertEqual(exist(input_file, 'file'), 2); + assertEqual(exist(output_file, 'file'), 2); + + file = file.rename('dry_run', false, 'force', true, 'verbose', false); + assertEqual(exist(input_file, 'file'), 0); + assertEqual(exist(output_file, 'file'), 2); + + teardown(input_file); + teardown(output_file); + +end + +function test_camel_case() + + filename = 'sub-01_ses-test_task-faceRecognition_run-02_bold.nii'; + file = bids.File(filename, 'use_schema', false); + + file.entities.task = 'test bla'; + assertEqual(file.filename, 'sub-01_ses-test_task-testBla_run-02_bold.nii'); + +end + function test_invalid_entity() % https://github.com/bids-standard/bids-matlab/issues/362 @@ -22,13 +178,10 @@ function test_invalid_entity() input.suffix = 'eeg'; input.ext = '.bdf'; input.entities.sub = '01'; - input.entities.task = '0.05'; - - assertExceptionThrown(@() bids.File(input, 'use_schema', false, 'tolerant', false), ... - 'File:InvalidEntityValue'); + input.entities.task = '0-0 .%5'; - assertWarning(@() bids.File(input, 'use_schema', true, 'tolerant', true, 'verbose', true), ... - 'File:InvalidEntityValue'); + bf = bids.File(input, 'use_schema', false, 'tolerant', false); + assertEqual(bf.filename, 'sub-01_task-005_eeg.bdf'); end @@ -151,6 +304,123 @@ function test_reorder() end +function test_reorder_schemaless() + + if bids.internal.is_octave + return + end + + filename = 'wuasub-01_task-faceRecognition_ses-test_run-02_bold.nii'; + file = bids.File(filename, 'use_schema', false); + file = file.reorder_entities(); + assertEqual(file.entity_order, {'sub' + 'ses' + 'sample' + 'task' + 'acq' + 'ce' + 'trc' + 'stain' + 'rec' + 'dir' + 'run' + 'mod' + 'echo' + 'flip' + 'inv' + 'mt' + 'part' + 'proc' + 'hemi' + 'space' + 'split' + 'recording' + 'chunk' + 'atlas' + 'res' + 'den' + 'label' + 'desc'}); + assertEqual(file.json_filename, 'wuasub-01_ses-test_task-faceRecognition_run-02_bold.json'); +end + +function test_reorder_schemaless_with_extra_entity() + + if bids.internal.is_octave + return + end + + filename = 'sub-01_foo-bar_task-face_ses-test_run-02_mask.nii'; + file = bids.File(filename, 'use_schema', false); + file = file.reorder_entities(); + assertEqual(file.json_filename, 'sub-01_ses-test_task-face_run-02_foo-bar_mask.json'); +end + +function test_reorder_with_schema() + filename = 'wuasub-01_task-faceRecognition_ses-test_run-02_bold.nii'; + file = bids.File(filename, 'use_schema', true); + file = file.reorder_entities(); + assertEqual(file.entity_order, {'sub' + 'ses' + 'task' + 'acq' + 'ce' + 'rec' + 'dir' + 'run' + 'echo' + 'part'}); + assertEqual(file.json_filename, 'wuasub-01_ses-test_task-faceRecognition_run-02_bold.json'); +end + +function test_create_file_with_schema_ignore_extra_entities() + name_spec.modality = 'fmap'; + name_spec.suffix = 'phasediff'; + name_spec.ext = '.json'; + name_spec.entities = struct('ses', 'test', ... + 'task', 'face', ... + 'run', '02', ... + 'acq', 'low', ... + 'sub', '01'); + file = bids.File(name_spec, 'use_schema', true); + assertEqual(file.filename, 'sub-01_ses-test_acq-low_run-02_phasediff.json'); +end + +function test_create_file_anat() + + name_spec.modality = 'anat'; + name_spec.suffix = 'T1w'; + name_spec.ext = '.json'; + + name_spec.entities = struct('ses', '01', ... + 'acq', 'FullExample', ... + 'run', '01', ... + 'sub', '01'); + + file = bids.File(name_spec, 'use_schema', true); + + assertEqual(file.filename, 'sub-01_ses-01_acq-FullExample_run-01_T1w.json'); + +end + +function test_create_file_meg() + + name_spec.modality = 'meg'; + name_spec.suffix = 'meg'; + name_spec.ext = '.json'; + name_spec.entities = struct('sub', '01', ... + 'acq', 'CTF', ... + 'ses', '01', ... + 'task', 'FullExample', ... + 'run', '1', ... + 'proc', 'sss'); + + bids_file = bids.File(name_spec, 'use_schema', true); + + assertEqual(bids_file.filename, 'sub-01_ses-01_task-FullExample_acq-CTF_run-1_proc-sss_meg.json'); + +end + function test_bids_file_derivatives_2() % GIVEN @@ -290,7 +560,7 @@ function test_error_no_extension() function test_name_validation() filename = 'wuasub-01_task-faceRecognition_ses-test_run-02_bold.nii'; - assertExceptionThrown(@() bids.File(filename, 'tolerant', false), ... + assertExceptionThrown(@() bids.File(filename, 'tolerant', false, 'use_schema', true), ... 'File:prefixDefined'); filename = 'bold.nii'; @@ -345,3 +615,16 @@ function test_validation() assertExceptionThrown(@() bf.validate_word('abc-def', 'Word'), ... 'File:InvalidWord'); end + +function set_up(filename) + if exist(filename, 'file') + delete(filename); + end + system(sprintf('touch %s', filename)); +end + +function teardown(filename) + if exist(filename, 'file') + delete(filename); + end +end diff --git a/tests/test_bids_init.m b/tests/test_bids_init.m index 5dbe298c..7fc30590 100644 --- a/tests/test_bids_init.m +++ b/tests/test_bids_init.m @@ -39,11 +39,56 @@ function test_folders() set_test_cfg(); folders.subjects = {'01', '02'}; - folders.sessions = {'test', 'retest'}; - folders.modalities = {'anat', 'func'}; + folders.sessions = {'test', 'retest', ''}; + folders.modalities = {'anat', 'func', 'fizz', ''}; bids.init('dummy_ds', 'folders', folders); assertEqual(exist(fullfile(pwd, 'dummy_ds', 'sub-02', 'ses-retest', 'func'), 'dir'), 7); + assertEqual(exist(fullfile(pwd, 'dummy_ds', 'sub-02', 'sub-02_sessions.tsv'), 'file'), 2); + + clean_up(); + +end + +function test_folders_no_session() + + set_test_cfg(); + + folders.subjects = {'01', '02'}; + folders.modalities = {'anat', 'func'}; + + bids.init('dummy_ds', 'folders', folders); + assertEqual(exist(fullfile(pwd, 'dummy_ds', 'sub-02', 'func'), 'dir'), 7); + assertEqual(exist(fullfile(pwd, 'dummy_ds', 'sub-02', 'sub-02_sessions.tsv'), 'file'), 0); + + clean_up(); + +end + +function test_validate() + + set_test_cfg(); + + folders.subjects = {'01-bla', '02_foo'}; + folders.sessions = {'te-st', 'ret$est'}; + folders.modalities = {'a#nat', 'fu*nc', '45^['}; + + assertExceptionThrown(@() bids.init('dummy_ds', 'folders', folders), ... + 'init:nonAlphaNumFodler'); + + folders.subjects = {'01', '02'}; + folders.sessions = {'te-st', 'ret$est'}; + folders.modalities = {'a#nat', 'fu*nc', '45^['}; + + assertExceptionThrown(@() bids.init('dummy_ds', 'folders', folders), ... + 'init:nonAlphaNumFodler'); + + folders.subjects = {'01', '02'}; + folders.sessions = {'test', 'retest'}; + folders.modalities = {'a#nat', 'fu*nc', '45^['}; + + assertExceptionThrown(@() bids.init('dummy_ds', 'folders', folders), ... + 'init:nonAlphaNumFodler'); clean_up(); @@ -79,6 +124,8 @@ function clean_up() pause(0.5); - rmdir(fullfile(pwd, 'dummy_ds'), 's'); + if isdir(fullfile(pwd, 'dummy_ds')) + rmdir(fullfile(pwd, 'dummy_ds'), 's'); + end end diff --git a/tests/test_bids_model.m b/tests/test_bids_model.m new file mode 100644 index 00000000..8ed0bc98 --- /dev/null +++ b/tests/test_bids_model.m @@ -0,0 +1,258 @@ +function test_suite = test_bids_model %#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_model_node_not_in_edges() + + if bids.internal.is_octave() + % TODO fix Octave error in CI + % failure: warning 'Octave:mixed-string-concat' was raised, + % expected 'Model:missingField'. Stack trace: + return + end + + bm = bids.Model('file', model_file('narps'), 'verbose', false); + + bm.Nodes{end + 1} = bm.Nodes{end}; + bm.Nodes{end}.Name = 'Foo'; + + bm.verbose = true; + assertWarning(@()bm.validate_edges(), 'Model:nodeMissingFromEdges'); + +end + +function test_model_load_edges() + + bm = bids.Model('file', model_file('narps'), 'verbose', false); + + edges = bm.Edges; + + assertEqual(edges{1}, struct('Source', 'run', 'Destination', 'subject')); + assertEqual(edges{2}, struct('Source', 'subject', 'Destination', 'positive')); + assertEqual(edges{3}, struct('Source', 'subject', ... + 'Destination', 'negative-loss', ... + 'Filter', struct('contrast', {{'loss'}}))); + assertEqual(edges{4}, struct('Source', 'subject', ... + 'Destination', 'between-groups', ... + 'Filter', struct('contrast', {{'loss'}}))); + +end + +function test_model_get_root_node() + + bm = bids.Model('file', model_file('narps'), 'verbose', false); + + [root_node, root_node_name] = bm.get_root_node(); + + assertEqual(root_node_name, 'run'); + +end + +function test_model_get_source_nodes() + + bm = bids.Model('file', model_file('narps'), 'verbose', false); + + assertEqual(bm.get_source_node('run'), {}); + + assertEqual(bm.get_source_node('subject'), bm.get_nodes('Name', 'run')); + + assertEqual(bm.get_source_node('negative-loss'), bm.get_nodes('Name', 'subject')); + +end + +function test_model_get_edge() + + bm = bids.Model('file', model_file('narps'), 'verbose', false); + + assertEqual(bm.get_edge('Source', 'run'), struct('Source', 'run', ... + 'Destination', 'subject')); + + assertEqual(numel(bm.get_edge('Source', 'subject')), 3); + + assertEqual(bm.get_edge('Destination', 'negative-loss'), ... + struct('Source', 'subject', ... + 'Destination', 'negative-loss', ... + 'Filter', struct('contrast', {{'loss'}}))); + +end + +function test_model_bug_385() + + bm = bids.Model('file', model_file('bug385'), 'verbose', false); + +end + +function test_model_basic() + + bm = bids.Model('file', model_file('narps'), 'verbose', false); + + assertEqual(bm.Name, 'NARPS'); + assertEqual(bm.Description, 'NARPS Analysis model'); + assertEqual(bm.BIDSModelVersion, '1.0.0'); + assertEqual(bm.Input, struct('task', {{'MGT'}})); + assertEqual(numel(bm.Nodes), 5); + assertEqual(numel(bm.Edges), 4); + assertEqual(bm.Edges{1}, struct('Source', 'run', 'Destination', 'subject')); + +end + +function test_model_default_model() + + if bids.internal.is_octave() + % TODO fix for octave in CI + return + end + + pth_bids_example = get_test_data_dir(); + BIDS = bids.layout(fullfile(pth_bids_example, 'ds003')); + + bm = bids.Model(); + bm = bm.default(BIDS); + + filename = fullfile(pwd, 'tmp', 'rhymejudgement.json'); + bm.write(filename); + + assertEqual(bids.util.jsondecode(filename), ... + bids.util.jsondecode(model_file('rhymejudgement'))); + delete(filename); + +end + +function test_model_default_no_events() + + if bids.internal.is_octave() + % TODO fix for octave in CI + return + end + + pth_bids_example = get_test_data_dir(); + BIDS = bids.layout(fullfile(pth_bids_example, 'asl001')); + + bm = bids.Model('verbose', false); + bm = bm.default(BIDS); + assertEqual(bm.Nodes{1}.Model.X, {'1'}); + +end + +function test_model_validate() + + if bids.internal.is_octave() + % TODO fix Octave error in CI + % failure: warning 'Octave:mixed-string-concat' was raised, + % expected 'Model:missingField'. Stack trace: + return + end + + bm = bids.Model(); + bm.Nodes{1} = rmfield(bm.Nodes{1}, 'Name'); + assertWarning(@()bm.validate(), 'Model:missingField'); + + bm = bids.Model(); + bm.Nodes{1}.Model = rmfield(bm.Nodes{1}.Model, 'X'); + assertWarning(@()bm.validate(), 'Model:missingField'); + + bm.Nodes{1}.Transformations = rmfield(bm.Nodes{1}.Transformations, 'Transformer'); + assertWarning(@()bm.validate(), 'Model:missingField'); + + bm.Nodes{1}.Contrasts = rmfield(bm.Nodes{1}.Contrasts{1}, 'ConditionList'); + assertWarning(@()bm.validate(), 'Model:missingField'); + +end + +function test_model_write() + + filename = fullfile(pwd, 'tmp', 'model-foo_smdl.json'); + + bm = bids.Model('file', model_file('narps'), 'verbose', false); + + bm.write(filename); + assertEqual(bids.util.jsondecode(model_file('narps')), ... + bids.util.jsondecode(filename)); + + delete(filename); + + bm = bids.Model('file', model_file('bug385'), 'verbose', false); + + bm.write(filename); + assertEqual(bids.util.jsondecode(model_file('bug385')), ... + bids.util.jsondecode(filename)); + + delete(filename); + +end + +function test_model_get_nodes() + + bm = bids.Model('file', model_file('narps'), 'verbose', false); + + assertEqual(numel(bm.get_nodes), 5); + assertEqual(numel(bm.get_nodes('Level', '')), 5); + assertEqual(numel(bm.get_nodes('Name', '')), 5); + assertEqual(numel(bm.get_nodes('Level', '', 'Name', '')), 5); + assertEqual(numel(bm.get_nodes('Level', 'Run')), 1); + assertEqual(numel(bm.get_nodes('Level', 'Dataset')), 3); + assertEqual(numel(bm.get_nodes('Name', 'negative-loss')), 1); + + if bids.internal.is_octave() + % TODO fix for octave in CI + return + end + + bm.verbose = true; + assertWarning(@()bm.get_nodes('Name', 'foo'), 'Model:missingNode'); + +end + +function test_model_get_design_matrix() + + bm = bids.Model('file', model_file('narps'), 'verbose', false); + + assertEqual(bm.get_design_matrix('Name', 'run'), ... + {'trials' + 'gain' + 'loss' + 'demeaned_RT' + 'rot_x' + 'rot_y' + 'rot_z' + 'trans_x' + 'trans_y' + 'trans_z' + 1}); + +end + +function test_model_node_level_getters() + + bm = bids.Model('file', model_file('narps'), 'verbose', false); + + assertEqual(bm.get_dummy_contrasts('Name', 'run'), ... + struct('Conditions', {{'trials'; 'gain'; 'loss'}}, ... + 'Test', 't')); + + assertEqual(fieldnames(bm.get_transformations('Name', 'run')), ... + {'Transformer'; 'Instructions'}); + + assertEqual(bm.get_contrasts('Name', 'negative-loss'), ... + {struct('Name', 'negative', 'ConditionList', 1, 'Weights', -1, 'Test', 't')}); + +end + +function test_model_empty_model() + + bm = bids.Model('init', true); + filename = fullfile(pwd, 'tmp', 'foo.json'); + bm.write(filename); + assertEqual(bids.util.jsondecode(filename), ... + bids.util.jsondecode(model_file('empty'))); + delete(filename); + +end + +function value = model_file(name) + value = fullfile(get_test_data_dir(), '..', 'data', 'model', ['model-' name '_smdl.json']); +end diff --git a/tests/test_bids_schema.m b/tests/test_bids_schema.m index 2491a417..e40510be 100644 --- a/tests/test_bids_schema.m +++ b/tests/test_bids_schema.m @@ -31,6 +31,16 @@ function test_get_datatypes() end +function test_return_entity_key() + + schema = bids.Schema(); + entity_key = schema.return_entity_key('description'); + assertEqual(entity_key, 'desc'); + + assertExceptionThrown(@()schema.return_entity_key('foo'), 'Schema:UnknownEnitity'); + +end + function test_return_entities_for_suffix_modality() schema = bids.Schema(); @@ -54,6 +64,24 @@ function test_find_suffix_group() end +function test_metadata_get_definition() + + schema = bids.Schema(); + def = schema.get_definition('onset'); + + assertEqual(def.name, 'onset'); + assertEqual(def.type, 'number'); + assertEqual(def.unit, 's'); + +end + +function test_metadata_object() + + schema = bids.Schema(); + assert(schema.eq); + +end + function test_return_modality_suffixes_regex schema = bids.Schema(); @@ -162,6 +190,45 @@ function test_schemaless() end +function test_return_entity_order_default + + schema = bids.Schema(); + + order = schema.entity_order(); + + expected = {'subject'; ... + 'session'; ... + 'sample'; ... + 'task'; ... + 'acquisition'; ... + 'ceagent'; ... + 'tracer'; ... + 'stain'; ... + 'reconstruction'; ... + 'direction'; ... + 'run'; ... + 'modality'; ... + 'echo'; ... + 'flip'; ... + 'inversion'; ... + 'mtransfer'; ... + 'part'; ... + 'processing'; ... + 'hemisphere'; ... + 'space'; ... + 'split'; ... + 'recording'; ... + 'chunk'; ... + 'atlas'; ... + 'resolution'; ... + 'density'; ... + 'label'; ... + 'description'}; + + assertEqual(order, expected); + +end + function test_return_entity_order schema = bids.Schema(); @@ -191,14 +258,16 @@ function test_schemaless() % entity_list_to_order = {'description' 'run' + 'foo' 'subject' - 'foo'}; + 'bar'}; order = schema.entity_order(entity_list_to_order); expected = {'subject' 'run' 'description' + 'bar' 'foo'}; assertEqual(order, expected); diff --git a/tests/test_copy_to_derivative.m b/tests/test_copy_to_derivative.m index 6a533f3f..dc49dc41 100644 --- a/tests/test_copy_to_derivative.m +++ b/tests/test_copy_to_derivative.m @@ -106,7 +106,7 @@ function test_copy_to_derivative_basic() function test_copy_to_derivative_unzip - [BIDS, out_path, filter, cfg] = fixture('MoAEpilot'); + [pth, out_path, filter, cfg, bu_folder] = fixture('MoAEpilot'); pipeline_name = 'bids-matlab'; unzip = true; @@ -115,7 +115,7 @@ function test_copy_to_derivative_basic() verbose = cfg.verbose; skip_dependencies = true; - bids.copy_to_derivative(BIDS, ... + bids.copy_to_derivative(pth, ... 'pipeline_name', pipeline_name, ... 'out_path', out_path, ... 'filter', filter, ... @@ -131,7 +131,7 @@ function test_copy_to_derivative_basic() zipped_files = bids.query(derivatives, 'data', 'extension', '.nii.gz'); assertEqual(numel(zipped_files), 0); - teardown(out_path); + teardown_moae(bu_folder); end @@ -220,7 +220,9 @@ function test_copy_to_derivative_dependencies() end -function [BIDS, out_path, filter, cfg] = fixture(dataset) +function [BIDS, out_path, filter, cfg, bu_folder] = fixture(dataset) + + bu_folder = ''; cfg = set_test_cfg(); @@ -263,12 +265,18 @@ function test_copy_to_derivative_dependencies() case 'MoAEpilot' - bids.util.mkdir(fullfile(get_test_data_dir(), '..', '..', 'demos', 'spm')); + BIDS = moae_dir(); - BIDS = bids.util.download_ds('source', 'spm', ... - 'demo', 'moae', ... - 'force', false, ... - 'verbose', false); + if ~isdir(fullfile(BIDS, 'sub-01')) + + bu_folder = fixture_moae(); + + bids.util.download_ds('source', 'spm', ... + 'demo', 'moae', ... + 'force', true, ... + 'verbose', false, ... + 'delete_previous', true); + end anat = fullfile(BIDS, 'sub-01', 'anat', 'sub-01_T1w.nii'); if exist(anat, 'file') diff --git a/tests/test_derivatives_json.m b/tests/test_derivatives_json.m index c7872221..4e38e0ab 100644 --- a/tests/test_derivatives_json.m +++ b/tests/test_derivatives_json.m @@ -22,6 +22,19 @@ function test_derivatives_json_basic() end +function test_derivatives_json_entity_as_number() + + if bids.internal.is_octave + return + end + + filename = 'sub-01_ses-test_task-faceRecognition_run-02_res-2_bold.nii'; + + assertWarning(@() bids.derivatives_json(filename), ... + 'derivatives_json:invalidFieldname'); + +end + function test_derivatives_json_force() %% force to create default content diff --git a/tests/test_diagnostic.m b/tests/test_diagnostic.m new file mode 100644 index 00000000..697d81b3 --- /dev/null +++ b/tests/test_diagnostic.m @@ -0,0 +1,31 @@ +function test_suite = test_diagnostic %#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_diagnostic_basic() + + close all; + + pth_bids_example = get_test_data_dir(); + + data_sets_to_test = '^ds10[13579]$'; % '^ds.*[0-9]$' + examples = bids.internal.file_utils('FPList', get_test_data_dir(), 'dir', data_sets_to_test); + + for i = 1:size(examples, 1) + + BIDS = bids.layout(deblank(examples(i, :))); + + diagnostic_table = bids.diagnostic(BIDS, 'output_path', pwd); + diagnostic_table = bids.diagnostic(BIDS, 'split_by', {'suffix'}, 'output_path', pwd); + diagnostic_table = bids.diagnostic(BIDS, 'split_by', {'task'}, 'output_path', pwd); + diagnostic_table = bids.diagnostic(BIDS, 'split_by', {'task', 'suffix'}, 'output_path', pwd); + + end + +end diff --git a/tests/test_report.m b/tests/test_report.m index f0491cdf..59562d1c 100644 --- a/tests/test_report.m +++ b/tests/test_report.m @@ -103,16 +103,25 @@ function test_report_pet() function test_report_moae_data() - % temporary silence - return + % no spm in CI + if bids.internal.is_github_ci() + return + end cfg = set_up(); cfg.read_nifti = true; - BIDS = fullfile(bids.internal.root_dir(), 'examples', 'MoAEpilot'); + bu_folder = fixture_moae(); - BIDS = bids.layout(BIDS, 'use_schema', true); + pth = bids.util.download_ds('source', 'spm', ... + 'demo', 'moae', ... + 'out_path', fullfile(pwd, 'tmp'), ... + 'force', true, ... + 'verbose', true, ... + 'delete_previous', false); + + BIDS = bids.layout(pth, 'use_schema', true); report = bids.report(BIDS, ... 'output_path', cfg.output_path, ... @@ -128,6 +137,8 @@ function test_report_moae_data() end assertEqual(content, expected); + teardown_moae(bu_folder); + end function expected = get_expected_content(cfg, dataset, modality) diff --git a/tests/test_transformers/test_transformers_class.m b/tests/test_transformers/test_transformers_class.m new file mode 100644 index 00000000..e128434f --- /dev/null +++ b/tests/test_transformers/test_transformers_class.m @@ -0,0 +1,62 @@ +function test_suite = test_transformers_class %#ok<*STOUT> + % + + % (C) Copyright 2022 Remi Gau + + 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_transformers_class_get_output() + + transformer = struct('Input', {{'onset'}}); + bt = bids.transformers_list.BaseTransformer(transformer); + assertEqual(bt.input, {'onset'}); + assertEqual(bt.output, {'onset'}); + +end + +function test_transformers_class_base() + + bt = bids.transformers_list.BaseTransformer(); + +end + +function test_transformers_class_get_input() + + bt = bids.transformers_list.BaseTransformer(); + assert(isempty(bt.get_input())); + + bt.input = bt.get_input(struct('Input', {{'onset', 'foo', 'bar'}})); + assertEqual(bt.input, {'onset', 'foo', 'bar'}); + + bt = bids.transformers_list.BaseTransformer(struct('Input', {{'onset', 'foo', 'bar'}})); + assertEqual(bt.input, {'onset', 'foo', 'bar'}); + +end + +function test_transformers_class_check_input() + + transformer = struct('Input', {{'onset', 'foo', 'bar'}}); + data = vis_motion_to_threshold_events(); + assertExceptionThrown(@()bids.transformers_list.BaseTransformer(transformer, data), ... + 'BaseTransformer:missingInput'); + +end + +%% Helper functions + +function cfg = set_up() + cfg = set_test_cfg(); + cfg.this_path = fileparts(mfilename('fullpath')); +end + +function value = dummy_data_dir() + cfg = set_up(); + value = fullfile(cfg.this_path, 'data', 'tsv_files'); +end diff --git a/tests/test_transformers/test_transformers_compute.m b/tests/test_transformers/test_transformers_compute.m new file mode 100644 index 00000000..67c043be --- /dev/null +++ b/tests/test_transformers/test_transformers_compute.m @@ -0,0 +1,612 @@ +function test_suite = test_transformers_compute %#ok<*STOUT> + % + + % (C) Copyright 2022 Remi Gau + + 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 + +%% COMPUTE + +function write_definition(input, output, trans, stack) + test_name = stack.name; + % write_test_definition_to_file(input, output, trans, test_name, 'compute'); +end + +%% multi step + +function test_multi_add_subtract_with_output + + % GIVEN + transformers(1).Name = 'Subtract'; + transformers(1).Input = 'onset'; + transformers(1).Value = 3; + transformers(1).Output = 'onset_minus_3'; + + transformers(2).Name = 'Add'; + transformers(2).Input = 'onset'; + transformers(2).Value = 1; + transformers(2).Output = 'onset_plus_1'; + + % WHEN + data = vis_motion_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assert(all(ismember({'onset_plus_1'; 'onset_minus_3'}, fieldnames(new_content)))); + assertEqual(new_content.onset_plus_1, [3; 5]); + assertEqual(new_content.onset_minus_3, [-1; 1]); + +end + +%% single step + +function test_Add_to_specific_rows + %% GIVEN + transformers(1).Name = 'Add'; + transformers(1).Input = 'onset'; + transformers(1).Query = 'familiarity == Famous face'; + transformers(1).Value = 3; + + % WHEN + data = face_rep_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.onset, [5; 4; 8; 8]); + +end + +function test_Subtract_to_specific_rows + + %% GIVEN + transformers(1).Name = 'Subtract'; + transformers(1).Input = 'onset'; + transformers(1).Query = 'response_time < 2'; + transformers(1).Value = 1; + + % WHEN + data = face_rep_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.onset, [1; 4; 4; 8]); + +end + +function test_Add_coerce_value + + %% GIVEN + transformers(1).Name = 'Add'; + transformers(1).Input = 'onset'; + transformers(1).Value = '3'; + + % WHEN + data = vis_motion_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.onset, [5; 7]); + + %% GIVEN + transformers(1).Name = 'Add'; + transformers(1).Input = 'onset'; + transformers(1).Value = '+'; + + % WHEN + assertExceptionThrown(@()bids.transformers(transformers, vis_motion_events()), ... + 'Basic:numericOrCoercableToNumericRequired'); + + % THEN + assertEqual(new_content.onset, [5; 7]); + +end + +function test_Constant_basic() + + %% GIVEN + transformers = struct('Name', 'Constant', ... + 'Output', 'cst'); + + % WHEN + data = vis_motion_to_threshold_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + assertEqual(new_content.cst, ones(4, 1)); + +end + +function test_Constant_with_value() + + %% GIVEN + transformers = struct('Name', 'Constant', ... + 'Value', 2, ... + 'Output', 'cst'); + + % WHEN + data = vis_motion_to_threshold_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + assertEqual(new_content.cst, ones(4, 1) * 2); + +end + +function test_Divide_several_inputs + + % GIVEN + transformers(1).Name = 'Divide'; + transformers(1).Input = {'onset', 'duration'}; + transformers(1).Value = 2; + + % WHEN + data = vis_motion_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.onset, [1; 2]); + assertEqual(new_content.duration, [1; 1]); + +end + +function test_Mean() + + % GIVEN + transformers = struct('Name', 'Mean', ... + 'Input', {{'age'}}); + + % WHEN + data = participants(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.age_mean, nan); + +end + +function test_Mean_with_output() + + if bids.internal.is_octave + return + end + + % GIVEN + transformers = struct('Name', 'Mean', ... + 'Input', {{'age'}}, ... + 'Output', 'age_mean_omitnan', ... + 'OmitNan', true); + + % WHEN + data = participants(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.age_mean_omitnan, 23.75); + +end + +function test_Product() + + % GIVEN + transformers = struct('Name', 'Product', ... + 'Input', {{'onset', 'duration'}}, ... + 'Output', 'onset_times_duration'); + + % WHEN + data = vis_motion_to_threshold_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.onset_times_duration, [4; 8; 12; 16]); + +end + +function test_StdDev() + + % GIVEN + transformers = struct('Name', 'StdDev', ... + 'Input', {{'age'}}); + + % WHEN + data = participants(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.age_std, nan); + +end + +function test_StdDev_omitnan() + + if bids.internal.is_octave + return + end + + % GIVEN + transformers = struct('Name', 'StdDev', ... + 'Input', {{'age'}}, ... + 'Output', 'age_std_omitnan', ... + 'OmitNan', true); + + % WHEN + data = participants(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertElementsAlmostEqual(new_content.age_std_omitnan, 15.543, 'absolute', 1e-3); + +end + +function test_Sum() + + % GIVEN + transformers = struct('Name', 'Sum', ... + 'Input', {{'onset', 'duration'}}, ... + 'Output', 'onset_plus_duration'); + + % WHEN + data = vis_motion_to_threshold_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.onset_plus_duration, [4; 6; 8; 10]); + +end + +function test_Sum_with_weights() + + % GIVEN + transformers = struct('Name', 'Sum', ... + 'Input', {{'onset', 'duration'}}, ... + 'Weights', [2, 1], ... + 'Output', 'onset_plus_duration_with_weight'); + + % WHEN + data = vis_motion_to_threshold_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.onset_plus_duration_with_weight, [6; 10; 14; 18]); + +end + +function test_Power + + %% GIVEN + transformers.Name = 'Power'; + transformers.Input = 'intensity'; + transformers.Value = 2; + + % WHEN + data = vis_motion_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.intensity, [4; 16]); +end + +function test_Power_with_output + + %% GIVEN + transformers.Name = 'Power'; + transformers.Input = 'intensity'; + transformers.Value = 3; + transformers.Output = 'intensity_cubed'; + + % WHEN + data = vis_motion_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.intensity_cubed, [8; -64]); + +end + +function test_Scale() + + % omit nan not implemented in octave + if bids.internal.is_octave + return + end + + %% GIVEN + transformers = struct('Name', 'Scale', ... + 'Input', {{'age'}}); + + % WHEN + data = participants(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertElementsAlmostEqual(new_content.age, ... + [-0.1769; -0.3699; 1.4315; -0.8846; nan], ... + 'absolute', 1e-3); + +end + +function test_Scale_all_options() + + % omit nan not implemented in octave + if bids.internal.is_octave + return + end + + %% GIVEN + transformers = struct('Name', 'Scale', ... + 'Input', {{'age'}}, ... + 'Demean', true, ... + 'Rescale', true, ... + 'ReplaceNa', 'off', ... + 'Output', {{'age_demeaned_centered'}}); + + % WHEN + data = participants(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertElementsAlmostEqual(new_content.age_demeaned_centered, ... + [-0.1769; -0.3699; 1.4315; -0.8846; nan], ... + 'absolute', 1e-3); +end + +function test_multi_Scale_nan_after() + + % omit nan not implemented in octave + if bids.internal.is_octave + return + end + + %% GIVEN + transformers{1} = struct('Name', 'Scale', ... + 'Input', {{'age'}}, ... + 'Rescale', false, ... + 'Output', {{'age_not_rescaled'}}); + transformers{2} = struct('Name', 'Scale', ... + 'Input', {{'age'}}, ... + 'Demean', false, ... + 'Output', {{'age_not_demeaned'}}); + transformers{3} = struct('Name', 'Scale', ... + 'Input', {{'age'}}, ... + 'ReplaceNa', 'after', ... + 'Rescale', false, ... + 'Output', {{'age_not_rescaled_after'}}); + transformers{4} = struct('Name', 'Scale', ... + 'Input', {{'age'}}, ... + 'ReplaceNa', 'after', ... + 'Demean', false, ... + 'Output', {{'age_not_demeaned_after'}}); + % WHEN + data = participants(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertElementsAlmostEqual(new_content.age_not_rescaled, ... + [-2.7500; -5.7500; 22.2500; -13.7500; nan], ... + 'absolute', 1e-3); + assertElementsAlmostEqual(new_content.age_not_demeaned, ... + [1.3511; 1.1581; 2.9595; 0.6434; nan], ... + 'absolute', 1e-3); + assertElementsAlmostEqual(new_content.age_not_rescaled_after, ... + [-2.7500; -5.7500; 22.2500; -13.7500; 0], ... + 'absolute', 1e-3); + assertElementsAlmostEqual(new_content.age_not_demeaned_after, ... + [1.3511; 1.1581; 2.9595; 0.6434; 0], ... + 'absolute', 1e-3); +end + +function test_multi_scale_nan_before() + + % omit nan not implemented in octave + if bids.internal.is_octave + return + end + + %% GIVEN + transformers{1} = struct('Name', 'Scale', ... + 'Input', {{'age'}}, ... + 'ReplaceNa', 'before', ... + 'Output', {{'age_before'}}); + transformers{2} = struct('Name', 'Scale', ... + 'Input', {{'age'}}, ... + 'ReplaceNa', 'before', ... + 'Rescale', false, ... + 'Output', {{'age_not_rescaled_before'}}); + transformers{3} = struct('Name', 'Scale', ... + 'Input', {{'age'}}, ... + 'ReplaceNa', 'before', ... + 'Demean', false, ... + 'Output', {{'age_not_demeaned_before'}}); + + % WHEN + data = participants(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertElementsAlmostEqual(new_content.age_before, ... + [0.1166; -0.0583; 1.5747; -0.5249; -1.1081], ... + 'absolute', 1e-3); + assertElementsAlmostEqual(new_content.age_not_rescaled_before, ... + [2; -1; 27; -9; -19], ... + 'absolute', 1e-3); + assertElementsAlmostEqual(new_content.age_not_demeaned_before, ... + [1.2247; 1.0498; 2.6828; 0.5832; 0], ... + 'absolute', 1e-3); +end + +function test_Subtract + + % GIVEN + transformers(1).Name = 'Subtract'; + transformers(1).Input = 'onset'; + transformers(1).Value = 3; + + % WHEN + data = vis_motion_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.onset, [-1; 1]); + +end + +function test_Threshold_with_output() + + transformers = struct('Name', 'Threshold', ... + 'Input', 'to_threshold', ... + 'Output', 'tmp'); + + data = vis_motion_to_threshold_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + assertEqual(new_content.tmp, [1; 2; 0; 0]); + +end + +function test_Threshold() + + %% WHEN + transformers = struct('Name', 'Threshold', ... + 'Input', 'to_threshold'); + + data = vis_motion_to_threshold_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.to_threshold, [1; 2; 0; 0]); + +end + +function test_Threshold_with_threshold_specified() + + %% WHEN + transformers = struct('Name', 'Threshold', ... + 'Input', 'to_threshold', ... + 'Threshold', 1); + + data = vis_motion_to_threshold_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.to_threshold, [0; 2; 0; 0]); + +end + +function test_Threshold_binarize() + + %% WHEN + transformers = struct('Name', 'Threshold', ... + 'Input', 'to_threshold', ... + 'Binarize', true); + + data = vis_motion_to_threshold_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.to_threshold, [1; 1; 0; 0]); + +end + +function test_Threshold_binarize_above() + + %% WHEN + transformers = struct('Name', 'Threshold', ... + 'Input', 'to_threshold', ... + 'Binarize', true, ... + 'Above', false); + + data = vis_motion_to_threshold_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.to_threshold, [0; 0; 1; 1]); + +end + +function test_Threshold_binarize_above_singed() + + %% WHEN + transformers = struct('Name', 'Threshold', ... + 'Input', 'to_threshold', ... + 'Threshold', 1, ... + 'Binarize', true, ... + 'Above', true, ... + 'Signed', false); + + data = vis_motion_to_threshold_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.to_threshold, [0; 1; 0; 1]); + +end + +%% Helper functions + +function cfg = set_up() + cfg = set_test_cfg(); + cfg.this_path = fileparts(mfilename('fullpath')); +end + +function value = dummy_data_dir() + cfg = set_up(); + value = fullfile(cfg.this_path, 'data', 'tsv_files'); +end diff --git a/tests/test_transformers/test_transformers_compute_logical.m b/tests/test_transformers/test_transformers_compute_logical.m new file mode 100644 index 00000000..ce503ffc --- /dev/null +++ b/tests/test_transformers/test_transformers_compute_logical.m @@ -0,0 +1,97 @@ +function test_suite = test_transformers_compute_logical %#ok<*STOUT> + % + + % (C) Copyright 2022 Remi Gau + + 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 write_definition(input, output, trans, stack, suffix) + + test_name = stack.name; + if nargin == 5 + test_name = [test_name '_' suffix]; + end + % write_test_definition_to_file(input, output, trans, test_name, 'compute'); + +end + +%% LOGICAL + +function test_And() + + % GIVEN + transformers = struct('Name', 'And', ... + 'Input', {{'sex_m', 'age_gt_twenty'}}, ... + 'Output', 'men_gt_twenty'); + + % WHEN + data = participants(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.men_gt_twenty, [true; false; false; false; false]); + +end + +function test_And_nan() + + % GIVEN + transformers = struct('Name', 'And', ... + 'Input', {{'handedness', 'age'}}, ... + 'Output', 'age_or_hand'); + + % WHEN + data = participants(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.age_or_hand, [true; true; false; true; false]); + +end + +function test_Or() + + % GIVEN + transformers = struct('Name', 'Or', ... + 'Input', {{'sex_m', 'age_gt_twenty'}}, ... + 'Output', 'men_or_gt_twenty'); + + % WHEN + data = participants(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.men_or_gt_twenty, [true; true; true; false; false]); + +end + +function test_Not() + + % GIVEN + transformers = struct('Name', 'Not', ... + 'Input', {{'age_gt_twenty'}}, ... + 'Output', 'ager_lt_twenty'); + + % WHEN + data = participants(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.ager_lt_twenty, [false; true; false; true; true]); + +end diff --git a/tests/test_transformers/test_transformers_munge.m b/tests/test_transformers/test_transformers_munge.m new file mode 100644 index 00000000..b3718a3a --- /dev/null +++ b/tests/test_transformers/test_transformers_munge.m @@ -0,0 +1,771 @@ +function test_suite = test_transformers_munge %#ok<*STOUT> + % + + % (C) Copyright 2022 Remi Gau + + 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 write_definition(input, output, trans, stack, suffix) + + test_name = stack.name; + if nargin == 5 + test_name = [test_name '_' suffix]; + end + % write_test_definition_to_file(input, output, trans, test_name, 'munge'); + +end + +%% single step + +% ordered alphabetically + +function test_Assign_with_target_attribute() + + transformers = struct('Name', 'Assign', ... + 'Input', 'response_time', ... + 'Target', 'Face', ... + 'TargetAttr', 'duration'); + + data = face_rep_events(); + data.Face = [1; 1; 1; 1]; + + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % check non involved fields are padded correctly + expected.familiarity = cat(1, data.familiarity, repmat({nan}, 4, 1)); + expected.onset = [data.onset; data.onset]; + + assertEqual(new_content.familiarity, expected.familiarity); + assertEqual(new_content.onset, expected.onset); + + % check involved fields + expected.response_time = [data.response_time; nan(size(data.response_time))]; + expected.Face = [nan(size(data.response_time)); data.Face]; + expected.duration = [data.duration; data.response_time]; + + assertEqual(new_content.response_time, expected.response_time); + assertEqual(new_content.Face, expected.Face); + assertEqual(new_content.duration, expected.duration); + +end + +function test_Assign() + + transformers = struct('Name', 'Assign', ... + 'Input', 'response_time', ... + 'Target', 'Face'); + + data = face_rep_events(); + data.Face = [1; 1; 1; 1]; + + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + assertEqual(new_content.Face, new_content.response_time); + +end + +function test_Assign_with_output() + + transformers = struct('Name', 'Assign', ... + 'Input', 'response_time', ... + 'Target', 'Face', ... + 'Output', 'new_face'); + + data = face_rep_events(); + data.Face = [1; 1; 1; 1]; + + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + assertEqual(new_content.new_face, new_content.response_time); + +end + +function test_Assign_with_output_and_input_attribute() + + transformers = struct('Name', 'Assign', ... + 'Input', 'response_time', ... + 'Target', 'Face', ... + 'Output', 'new_face', ... + 'InputAttr', 'onset'); + + data = face_rep_events(); + data.Face = [1; 1; 1; 1]; + + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + assertEqual(new_content.new_face, new_content.onset); + +end + +function test_Assign_missing_target() + + transformers = struct('Name', 'Assign', ... + 'Input', 'response_time', ... + 'Target', 'Face'); + + assertExceptionThrown(@()bids.transformers(transformers, face_rep_events()), ... + 'check_field:missingTarget'); + +end + +function test_Concatenate() + + % GIVEN + tsvFile = fullfile(dummy_data_dir(), ... + 'sub-01_task-FaceRepetitionBefore_events.tsv'); + tsv_content = bids.util.tsvread(tsvFile); + + transformers = struct('Name', 'Concatenate', ... + 'Input', {{'face_type', 'repetition_type'}}, ... + 'Output', 'trial_type'); + + % WHEN + data = tsv_content; + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + assertEqual(unique(new_content.trial_type), ... + {'famous_1'; 'famous_2'; 'unfamiliar_1'; 'unfamiliar_2'}); + +end + +function test_Concatenate_strings() + + % GIVEN + transformers = struct('Name', 'Concatenate', ... + 'Input', {{'trial_type', 'familiarity'}}, ... + 'Output', 'trial_type'); + + % WHEN + data = face_rep_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + assertEqual(unique(new_content.trial_type), ... + {'Face_Famous face'; ... + 'Face_Unfamiliar face'}); + +end + +function test_Concatenate_numbers() + + % GIVEN + transformers = struct('Name', 'Concatenate', ... + 'Input', {{'onset', 'response_time'}}, ... + 'Output', 'trial_type'); + + % WHEN + data = face_rep_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + assertEqual(unique(new_content.trial_type), ... + {'2_1.5' + '4_2' + '5_1.56' + '8_2.1'}); + +end + +function test_Copy() + + % GIVEN + tsvFile = fullfile(dummy_data_dir(), 'sub-01_task-FaceRepetitionBefore_events.tsv'); + tsv_content = bids.util.tsvread(tsvFile); + + transformers = struct('Name', 'Copy', ... + 'Input', {{'face_type', 'repetition_type'}}, ... + 'Output', {{'foo', 'bar'}}); + data = tsv_content; + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + assert(all(ismember({'foo'; 'bar'}, fieldnames(new_content)))); + assertEqual(new_content.foo, new_content.face_type); + assertEqual(new_content.bar, new_content.repetition_type); + +end + +function test_Delete() + + % GIVEN + tsvFile = fullfile(dummy_data_dir(), 'sub-01_task-FaceRepetitionBefore_events.tsv'); + tsv_content = bids.util.tsvread(tsvFile); + + transformers = struct('Name', 'Delete', ... + 'Input', 'face_type'); + + data = tsv_content; + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + assert(~(ismember({'face_type'}, fieldnames(new_content)))); + +end + +function test_DropNA() + + % GIVEN + transformers = struct('Name', 'DropNA', ... + 'Input', {{'age', 'handedness'}}); + + % WHEN + data = participants(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.age, [21; 18; 46; 10]); + assertEqual(new_content.handedness, {'right'; 'left'; 'left'; 'right'}); + +end + +function test_Factor() + + % GIVEN + transformers = struct('Name', 'Factor', ... + 'Input', {{'familiarity'}}); + + % WHEN + data = face_rep_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assert(isfield(new_content, 'familiarity_1')); + assert(isfield(new_content, 'familiarity_2')); + assertEqual(new_content.familiarity_1, [true; false; true; false]); + assertEqual(new_content.familiarity_2, [false; true; false; true]); + +end + +function test_Factor_numeric() + + % GIVEN + transformers = struct('Name', 'Factor', ... + 'Input', {{'age'}}); + + % WHEN + data = participants(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assert(isfield(new_content, 'age_10')); + assert(isfield(new_content, 'age_NaN')); + assertEqual(new_content.age_10, [false; false; false; true; false]); + assertEqual(new_content.age_NaN, [false; false; false; false; true]); + +end + +function test_Filter_numeric() + + types = {'>=', '<=', '==', '>', '<', '~='}; + expected = [nan 2 1.56 2.1 + 1.5 nan 1.56 nan + nan nan 1.56 nan + nan 2 nan 2.1 + 1.5 nan nan nan + 1.5 2 nan 2.1]; + + for i = 1:numel(types) + + % GIVEN + transformers = struct('Name', 'Filter', ... + 'Input', 'response_time', ... + 'Query', [' response_time ' types{i} ' 1.56']); + + % WHEN + data = face_rep_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st, types{i}); + + % THEN + assertEqual(new_content.response_time, expected(i, :)'); + + end + +end + +function test_Filter_string() + + % GIVEN + transformers = struct('Name', 'Filter', ... + 'Input', 'familiarity', ... + 'Query', ' familiarity == Famous face '); + + % WHEN + data = face_rep_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.familiarity, {'Famous face'; nan; 'Famous face'; nan}); + +end + +function test_Filter_string_unequal() + + % GIVEN + transformers = struct('Name', 'Filter', ... + 'Input', 'familiarity', ... + 'Query', ' familiarity ~= Famous face '); + + % WHEN + data = face_rep_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.familiarity, {nan; 'Unfamiliar face'; nan; 'Unfamiliar face'}); + +end + +function test_Filter_string_output() + + % GIVEN + transformers = struct('Name', 'Filter', ... + 'Input', 'familiarity', ... + 'Query', ' familiarity == Famous face ', ... + 'Output', 'famous_face'); + + % WHEN + data = face_rep_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.familiarity, {'Famous face' + 'Unfamiliar face' + 'Famous face' + 'Unfamiliar face'}); + assertEqual(new_content.famous_face, {'Famous face'; nan'; 'Famous face'; nan}); + +end + +function test_Filter_string_output_across_columns() + + % GIVEN + transformers = struct('Name', 'Filter', ... + 'Input', 'onset', ... + 'Query', ' familiarity == Famous face ', ... + 'Output', 'new'); + + % WHEN + data = face_rep_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.new, [2; nan; 5; nan]); + +end + +function test_Filter_across_columns() + + transformers = struct('Name', 'Filter', ... + 'Input', 'familiarity', ... + 'Query', 'repetition==1', .... + 'Output', 'familiarity_repetition_1'); + + % WHEN + new_content = bids.transformers(transformers, face_rep_events); + + % THEN + assertEqual(new_content.familiarity_repetition_1, ... + {'Famous face'; 'Unfamiliar face'; nan; nan}); + +end + +function test_Filter_several_inputs() + + transformers = struct('Name', 'Filter', ... + 'Input', {{'repetition', 'response_time'}}, ... + 'Query', 'repetition>1'); + + % WHEN + data = face_rep_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + assertEqual(new_content.repetition, [nan; nan; 2; 2]); + + assertEqual(new_content.response_time, [nan; nan; 1.56; 2.1]); + +end + +function test_LabelIdenticalRows_rows + + transformers(1).Name = 'LabelIdenticalRows'; + transformers(1).Input = {'trial_type', 'stim_type', 'other_type'}; + + data.trial_type = {'face'; 'face'; 'house'; 'house'; 'house'; 'house'; 'house'; 'chair'}; + data.stim_type = [1; 1; 1; 2; nan; 5; 2; nan]; + data.other_type = {'face'; 1; 1; 2; nan; 'chair'; 'chair'; nan}; + + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + assertEqual(new_content.trial_type_label, [1; 2; 1; 2; 3; 4; 5; 1]); + assertEqual(new_content.stim_type_label, [1; 2; 3; 1; 1; 1; 1; 1]); + assertEqual(new_content.other_type_label, [1; 1; 2; 1; 1; 1; 2; 1]); + +end + +function test_LabelIdenticalRows_rows_cumulative + + transformers(1).Name = 'LabelIdenticalRows'; + transformers(1).Input = {'trial_type'}; + transformers(1).Cumulative = true; + + data.trial_type = {'face'; 'face'; 'house'; 'house'; 'face'; 'house'; 'chair'}; + + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + assertEqual(new_content.trial_type_label, [1; 2; 1; 2; 3; 3; 1]); + +end + +function test_MergeIdenticalRows_rows_cellstr + + transformers(1).Name = 'MergeIdenticalRows'; + transformers(1).Input = {'trial_type'}; + + data.trial_type = {'house'; 'face'; 'face'; 'house'; 'chair'; 'house'; 'chair'}; + data.duration = [1; 1; 1; 1; 1; 1; 1]; + data.onset = [3; 1; 2; 6; 8; 4; 7]; + data.stim_type = {'delete'; 'delete'; 'keep'; 'keep'; 'keep'; 'delete'; 'delete'}; + + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + assertEqual(new_content.trial_type, {'face'; 'house'; 'chair'}); + assertEqual(new_content.stim_type, {'keep'; 'keep'; 'keep'}); + assertEqual(new_content.onset, [1; 3; 7]); + assertEqual(new_content.duration, [2; 4; 2]); + +end + +function test_MergeIdenticalRows_rows_numeric + + transformers(1).Name = 'MergeIdenticalRows'; + transformers(1).Input = {'trial_type'}; + + data.trial_type = [1; 2; 2; nan; 1; 3; 3]; + data.duration = [1; 1; 1; 1; 1; 1; 1]; + data.onset = [3; 1; 2; 6; 8; 4; 7]; + data.stim_type = {'keep'; 'delete'; 'keep'; 'keep'; 'keep'; 'keep'; 'keep'}; + + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + assertEqual(new_content.trial_type, [2; 1; 3; nan; 3; 1]); + assertEqual(new_content.stim_type, {'keep'; 'keep'; 'keep'; 'keep'; 'keep'; 'keep'}); + assertEqual(new_content.onset, [1; 3; 4; 6; 7; 8]); + assertEqual(new_content.duration, [2; 1; 1; 1; 1; 1]); + +end + +function test_Replace() + + %% GIVEN + transformers(1).Name = 'Replace'; + transformers(1).Input = 'familiarity'; + transformers(1).Replace(1) = struct('key', 'Famous face', 'value', 'foo'); + transformers(1).Replace(2) = struct('key', 'Unfamiliar face', 'value', 'bar'); + + % WHEN + data = face_rep_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.familiarity, {'foo'; 'bar'; 'foo'; 'bar'}); + +end + +function test_Replace_regexp() + + %% GIVEN + transformers(1).Name = 'Replace'; + transformers(1).Input = 'familiarity'; + transformers(1).Replace(1) = struct('key', '.*face', 'value', 'foo'); + + % WHEN + data = face_rep_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.familiarity, {'foo'; 'foo'; 'foo'; 'foo'}); + +end + +function test_Replace_string_by_numeric() + + %% GIVEN + transformers(1).Name = 'Replace'; + transformers(1).Input = 'familiarity'; + transformers(1).Replace(1).key = 'Famous face'; + transformers(1).Replace(1).value = 1; + transformers(1).Attribute = 'duration'; + + % WHEN + data = face_rep_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.duration, [1; 2; 1; 2]); + +end + +function test_Replace_with_output() + + %% GIVEN + transformers(1).Name = 'Replace'; + transformers(1).Input = 'familiarity'; + transformers(1).Output = 'tmp'; + transformers(1).Replace(1).key = 'Famous face'; + transformers(1).Replace(1).value = 1; + transformers(1).Attribute = 'all'; + + % WHEN + data = face_rep_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.tmp, {1; 'Unfamiliar face'; 1; 'Unfamiliar face'}); + assertEqual(new_content.duration, [1; 2; 1; 2]); + assertEqual(new_content.onset, [1; 4; 1; 8]); + +end + +function test_Replace_string_in_numeric_output() + + %% GIVEN + data.fruits = {'apple'; 'banana'; 'elusive'}; + data.onset = {1; 2; 3}; + data.duration = {0; 1; 3}; + + replace = struct('key', {'apple'; 'elusive'}, 'value', -1); + replace(end + 1).key = -1; + replace(end).value = 0; + + transformer = struct('Name', 'Replace', ... + 'Input', 'fruits', ... + 'Attribute', 'all', ... + 'Replace', replace); + + % WHEN + new_content = bids.transformers(transformer, data); + + % THEN + assertEqual(new_content.fruits, {0; 'banana'; 0}); + assertEqual(new_content.onset, {0; 2; 0}); + assertEqual(new_content.duration, {0; 1; 0}); + +end + +function test_Rename() + + % GIVEN + tsvFile = fullfile(dummy_data_dir(), 'sub-01_task-FaceRepetitionBefore_events.tsv'); + tsv_content = bids.util.tsvread(tsvFile); + + transformers = struct('Name', 'Rename', ... + 'Input', {{'face_type', 'repetition_type'}}, ... + 'Output', {{'foo', 'bar'}}); + data = tsv_content; + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + assert(all(ismember({'foo'; 'bar'}, fieldnames(new_content)))); + assert(all(~ismember({'face_type'; 'repetition_type'}, fieldnames(new_content)))); + assertEqual(new_content.foo, tsv_content.face_type); + assertEqual(new_content.bar, tsv_content.repetition_type); + +end + +function test_Select() + + % GIVEN + transformers = struct('Name', 'Select', ... + 'Input', {{'age'}}); + + % WHEN' + data = participants(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(fieldnames(new_content), {'age'}); + +end + +function test_Select_events_file() + + % GIVEN + tsvFile = fullfile(dummy_data_dir(), 'sub-01_task-FaceRepetitionBefore_events.tsv'); + tsv_content = bids.util.tsvread(tsvFile); + + % GIVEN + transformers = struct('Name', 'Select', ... + 'Input', 'face_type'); + + data = tsv_content; + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + assertEqual(fieldnames(new_content), {'face_type' + 'onset' + 'duration'}); + +end + +function test_Select_events_file_2() + + % GIVEN + transformers = struct('Name', 'Select', ... + 'Input', {{'familiarity'}}); + + % WHEN' + data = face_rep_events(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(fieldnames(new_content), {'familiarity'; 'onset'; 'duration'}); + +end + +function test_Split_empty_by() + + % GIVEN + transformers = struct('Name', 'Split', ... + 'Input', {{'age'}}, ... + 'By', {{}}); + + % WHEN' + data = participants(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content, participants); + +end + +function test_Split_simple() + + % GIVEN + transformers = struct('Name', 'Split', ... + 'Input', {{'age'}}, ... + 'By', {{'sex'}}); + + % WHEN' + data = participants(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.age_BY_sex_M, [21; 18; nan; nan; nan]); + assertEqual(new_content.age_BY_sex_F, [nan; nan; 46; 10; nan]); + +end + +function test_Split_simple_string() + + % GIVEN + transformers = struct('Name', 'Split', ... + 'Input', {{'handedness'}}, ... + 'By', {{'sex'}}); + + % WHEN' + data = participants(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assertEqual(new_content.handedness_BY_sex_F, {nan; nan; nan; 'left'; 'right'}); + assertEqual(new_content.handedness_BY_sex_M, {'right'; 'left'; nan; nan; nan}); + +end + +function test_Split_nested() + + % GIVEN + transformers = struct('Name', 'Split', ... + 'Input', {{'age'}}, ... + 'By', {{'sex', 'handedness'}}); + + % WHEN' + data = participants(); + new_content = bids.transformers(transformers, data); + st = dbstack; + write_definition(data, new_content, transformers, st); + + % THEN + assert(isfield(new_content, 'age_BY_handedness_left_BY_sex_M')); + assertEqual(numel(fieldnames(new_content)), 11); + assertEqual(new_content.age_BY_handedness_left_BY_sex_M, [NaN; 18; NaN; NaN; NaN]); + +end + +%% Helper functions + +function cfg = set_up() + cfg = set_test_cfg(); + cfg.this_path = fileparts(mfilename('fullpath')); +end + +function value = dummy_data_dir() + cfg = set_up(); + value = fullfile(cfg.this_path, '..', 'data', 'tsv_files'); +end diff --git a/tests/test_transformers/test_transformers_munge_multi.m b/tests/test_transformers/test_transformers_munge_multi.m new file mode 100644 index 00000000..cc58d31e --- /dev/null +++ b/tests/test_transformers/test_transformers_munge_multi.m @@ -0,0 +1,135 @@ +function test_suite = test_transformers_munge_multi %#ok<*STOUT> + % + + % (C) Copyright 2022 Remi Gau + + 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 + +%% MUNGE + +%% multi step + +function test_multi_touch() + + % GIVEN + tsvFile = fullfile(dummy_data_dir(), 'sub-01_task-TouchBefore_events.tsv'); + tsv_content = bids.util.tsvread(tsvFile); + + transformers{1} = struct('Name', 'Threshold', ... + 'Input', 'duration', ... + 'Binarize', true, ... + 'Output', 'tmp'); + transformers{2} = struct('Name', 'Replace', ... + 'Input', 'tmp', ... + 'Output', 'duration', ... + 'Attribute', 'duration', ... + 'Replace', struct('key', 1, ... + 'value', 1)); + transformers{3} = struct('Name', 'Delete', ... + 'Input', {{'tmp'}}); + + % WHEN + [new_content, json] = bids.transformers(transformers, tsv_content); + + % THEN + % TODO assert whole content + assertEqual(fieldnames(tsv_content), fieldnames(new_content)); + assertEqual(json, struct('Transformer', ['bids-matlab_' bids.internal.get_version], ... + 'Instructions', {transformers})); + +end + +function test_multi_combine_columns() + + % GIVEN + tsvFile = fullfile(dummy_data_dir(), 'sub-01_task-FaceRepetitionBefore_events.tsv'); + tsv_content = bids.util.tsvread(tsvFile); + + transformers{1} = struct('Name', 'Filter', ... + 'Input', 'face_type', ... + 'Query', 'face_type==famous', ... + 'Output', 'Famous'); + transformers{2} = struct('Name', 'Filter', ... + 'Input', 'repetition_type', ... + 'Query', 'repetition_type==1', ... + 'Output', 'FirstRep'); + transformers{3} = struct('Name', 'And', ... + 'Input', {{'Famous', 'FirstRep'}}, ... + 'Output', 'tmp'); + transformers{4} = struct('Name', 'Replace', ... + 'Input', 'tmp', ... + 'Output', 'trial_type', ... + 'Replace', struct('key', 'tmp_1', ... + 'value', 'FamousFirstRep')); + transformers{5} = struct('Name', 'Delete', ... + 'Input', {{'tmp', 'Famous', 'FirstRep'}}); + + % WHEN + data = tsv_content; + new_content = bids.transformers(transformers, data); + + % THEN + % TODO assert whole content + assertEqual(fieldnames(tsv_content), fieldnames(new_content)); + assertEqual(unique(new_content.trial_type), {'face'}); + +end + +function test_multi_complex_filter_with_and() + + %% GIVEN + tsvFile = fullfile(dummy_data_dir(), 'sub-01_task-FaceRepetitionBefore_events.tsv'); + tsv_content = bids.util.tsvread(tsvFile); + + transformers{1} = struct('Name', 'Filter', ... + 'Input', 'face_type', ... + 'Query', 'face_type==famous', ... + 'Output', 'Famous'); + transformers{2} = struct('Name', 'Filter', ... + 'Input', 'repetition_type', ... + 'Query', 'repetition_type==1', ... + 'Output', 'FirstRep'); + + % WHEN + data = tsv_content; + new_content = bids.transformers(transformers, data); + + % THEN + assert(all(ismember({'Famous'; 'FirstRep'}, fieldnames(new_content)))); + assertEqual(sum(strcmp(new_content.Famous, 'famous')), 52); + if ~bids.internal.is_octave + assertEqual(nansum(new_content.FirstRep), 52); + end + + %% GIVEN + transformers{3} = struct('Name', 'And', ... + 'Input', {{'Famous', 'FirstRep'}}, ... + 'Output', 'FamousFirstRep'); + + % WHEN + data = tsv_content; + new_content = bids.transformers(transformers, data); + + % THEN + assertEqual(sum(new_content.FamousFirstRep), 26); + +end + +%% Helper functions + +function cfg = set_up() + cfg = set_test_cfg(); + cfg.this_path = fileparts(mfilename('fullpath')); +end + +function value = dummy_data_dir() + cfg = set_up(); + value = fullfile(cfg.this_path, '..', 'data', 'tsv_files'); +end diff --git a/tests/test_transformers/test_transformers_side_functions.m b/tests/test_transformers/test_transformers_side_functions.m new file mode 100644 index 00000000..e39ee5d9 --- /dev/null +++ b/tests/test_transformers/test_transformers_side_functions.m @@ -0,0 +1,70 @@ +function test_suite = test_transformers_side_functions %#ok<*STOUT> + % + + % (C) Copyright 2022 Remi Gau + + 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_no_transformation() + + transformers = struct([]); + + [new_content, json] = bids.transformers(transformers, participants()); + + assertEqual(new_content, participants()); + + assertEqual(json, struct('Transformer', ['bids-matlab_' bids.internal.get_version], ... + 'Instructions', struct([]))); + +end + +%% SIDE FUNCTIONS + +function test_get_input() + + %% GIVEN + transformers = struct('Input', {{'onset'}}); + data = vis_motion_to_threshold_events(); + + % WHEN + inputs = bids.transformers_list.get_input(transformers, data); + + assertEqual(inputs, {'onset'}); + + %% GIVEN + transformers = struct('Input', {{'onset', 'foo', 'bar'}}, 'tolerant', false); + data = vis_motion_to_threshold_events(); + + % WHEN + assertExceptionThrown(@()bids.transformers_list.get_input(transformers, data), ... + 'check_field:missingInput'); + +end + +function status = test_is_run_level() + + data = struct('onset', [], 'duration', [], 'foo', 'bar'); + assert(bids.transfomers.is_run_level(data)); + +end + +function test_get_query() + + transformer.Query = 'R T == 1'; + + [left, type, right] = bids.transformers_list.get_query(transformer); + + assertEqual(type, '=='); + assertEqual(left, 'R T'); + assertEqual(right, '1'); + +end diff --git a/tests/tests_layout/test_layout.m b/tests/tests_layout/test_layout.m index 30eb4c83..ec9070f1 100644 --- a/tests/tests_layout/test_layout.m +++ b/tests/tests_layout/test_layout.m @@ -6,12 +6,63 @@ initTestSuite; end +function test_layout_filter() + + verbose = false; + + BIDS = bids.layout(fullfile(get_test_data_dir(), '7t_trt'), ... + 'verbose', verbose, ... + 'filter', struct('sub', {{'01', '02'}}, ... + 'modality', {{'anat', 'func'}}, ... + 'ses', {{'1', '2'}})); + + subjects = bids.query(BIDS, 'subjects'); + assertEqual(subjects, {'01', '02'}); + + subjects = bids.query(BIDS, 'modalities'); + assertEqual(subjects, {'anat', 'func'}); + + subjects = bids.query(BIDS, 'sessions'); + assertEqual(subjects, {'1', '2'}); + +end + +function test_layout_filter_regex() + + verbose = false; + + BIDS = bids.layout(fullfile(get_test_data_dir(), '7t_trt'), ... + 'verbose', verbose, ... + 'filter', struct('sub', {{'^.*0[12]'}}, ... + 'modality', {{'anat', 'func'}}, ... + 'ses', {{'[1]'}})); + + subjects = bids.query(BIDS, 'subjects'); + assertEqual(subjects, {'01', '02'}); + + subjects = bids.query(BIDS, 'modalities'); + assertEqual(subjects, {'anat', 'func'}); + + subjects = bids.query(BIDS, 'sessions'); + assertEqual(subjects, {'1'}); + +end + +function test_layout_empty_subject_folder_allowed_when_schemaless() + + verbose = false; + + bids.util.mkdir(fullfile(pwd, 'tmp/sub-01')); + bids.layout(fullfile(pwd, 'tmp'), 'use_schema', false, 'verbose', verbose); + rmdir(fullfile(pwd, 'tmp'), 's'); +end + function test_layout_smoke_test() - pth_bids_example = get_test_data_dir(); + verbose = false; - BIDS = bids.layout(fullfile(pth_bids_example, 'genetics_ukbb')); + BIDS = bids.layout(fullfile(get_test_data_dir(), 'genetics_ukbb'), 'verbose', verbose); - BIDS = bids.layout(fullfile(pth_bids_example, 'ds210')); + BIDS = bids.layout(fullfile(get_test_data_dir(), 'ds210'), 'verbose', verbose); end diff --git a/tests/tests_layout/test_layout_derivatives.m b/tests/tests_layout/test_layout_derivatives.m index 5f5753ff..812857cb 100644 --- a/tests/tests_layout/test_layout_derivatives.m +++ b/tests/tests_layout/test_layout_derivatives.m @@ -27,9 +27,22 @@ function test_layout_nested() pth_bids_example = get_test_data_dir(); - BIDS = bids.layout(fullfile(pth_bids_example, 'ds000117'), ... - 'use_schema', true, ... - 'index_derivatives', true); + dataset_to_test = {'ds000117' + 'qmri_irt1' + 'qmri_mese' + 'qmri_mp2rage' + 'qmri_mp2rageme' + 'qmri_mtsat' + 'qmri_sa2rage' + 'qmri_vfa' + 'qmri_mpm'}; + + for i = 1:numel(dataset_to_test) + BIDS = bids.layout(fullfile(pth_bids_example, dataset_to_test{i}), ... + 'use_schema', true, 'tolerant', false, ... + 'index_derivatives', true); + fprintf(1, '.'); + end end diff --git a/tests/tests_private/test_get_metadata.m b/tests/tests_private/test_get_metadata.m index 5ab12983..44f7035a 100644 --- a/tests/tests_private/test_get_metadata.m +++ b/tests/tests_private/test_get_metadata.m @@ -44,6 +44,30 @@ function test_get_metadata_basic() end +function test_get_metadata_basic_File() + + % Same as above but uses the bids.File class + + pth = fullfile(fileparts(mfilename('fullpath')), '..', 'data', 'synthetic'); + + func_sub_01.RepetitionTime = 10; + anat_sub_01.FlipAngle = 10; + anat_sub_01.Manufacturer = 'Siemens'; + + % try to get metadata + BIDS = bids.layout(pth); + + data = bids.query(BIDS, 'data', 'sub', '01', 'suffix', 'bold'); + bf = bids.File(data{1}); + assert(bf.metadata.RepetitionTime == func_sub_01.RepetitionTime); + + %% test anat metadata subject 01 + metadata = bids.query(BIDS, 'data', 'sub', '01', 'suffix', 'T1w'); + bf = bids.File(data{1}); + assertEqual(bf.metadata.Manufacturer, anat_sub_01.Manufacturer); + +end + function test_get_metadata_internal() pth_bids_example = get_test_data_dir(); @@ -69,3 +93,27 @@ function test_get_metadata_participants() assertEqual(metadata, expected_metadata); end + +function test_get_metadata_participants_File() + + pth_bids_example = fullfile(get_test_data_dir(), 'pet002'); + + file = fullfile(pth_bids_example, 'participants.tsv'); + side_car = fullfile(pth_bids_example, 'participants.json'); + + bf = bids.File(file); + + expected_metadata = bids.util.jsondecode(side_car); + assertEqual(bf.metadata, expected_metadata); + + subj_folder = fullfile('sub-02', 'ses-baseline', 'pet'); + + file = fullfile(pth_bids_example, subj_folder, 'sub-02_ses-baseline_pet.nii.gz'); + side_car = fullfile(pth_bids_example, subj_folder, 'sub-02_ses-baseline_pet.json'); + + bf = bids.File(file); + + expected_metadata = bids.util.jsondecode(side_car); + assertEqual(bf.metadata, expected_metadata); + +end diff --git a/tests/tests_private/test_get_metadata_suffixes.m b/tests/tests_private/test_get_metadata_suffixes.m index 8796c449..8a8fbd76 100644 --- a/tests/tests_private/test_get_metadata_suffixes.m +++ b/tests/tests_private/test_get_metadata_suffixes.m @@ -15,6 +15,9 @@ function test_get_metadata_suffixes_basic() side_car = fullfile(data_dir, 'sub-06_hemi-R_space-individual_den-native_thickness.json'); metalist = bids.internal.get_meta_list(file); + % TODO only only json file per folder level allowed + % assertEqual(numel(metalist), 1) + metadata = bids.internal.get_metadata(metalist); expected_metadata = bids.util.jsondecode(side_car); diff --git a/tests/tests_private/test_list_all_trial_types.m b/tests/tests_private/test_list_all_trial_types.m new file mode 100644 index 00000000..28b32eb6 --- /dev/null +++ b/tests/tests_private/test_list_all_trial_types.m @@ -0,0 +1,62 @@ +function test_suite = test_list_all_trial_types %#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_list_all_trial_types_basic() + + pth_bids_example = get_test_data_dir(); + + BIDS = fullfile(pth_bids_example, 'ds001'); + + %% dependencies + trial_type_list = bids.internal.list_all_trial_types(BIDS, 'balloonanalogrisktask'); + expected = {'cash_demean'; ... + 'control_pumps_demean'; ... + 'explode_demean'; ... + 'pumps_demean'}; + assertEqual(trial_type_list, expected); + +end + +function test_list_all_trial_types_warning() + + pth_bids_example = get_test_data_dir(); + + BIDS = fullfile(pth_bids_example, 'ds001'); + + %% dependencies + trial_type_list = bids.internal.list_all_trial_types(BIDS, {'not', 'a', 'task'}, ... + 'verbose', false); + assertEqual(trial_type_list, {}); + if bids.internal.is_octave + return + end + assertWarning(@() bids.internal.list_all_trial_types(BIDS, {'not', 'a', 'task'}, ... + 'verbose', true), ... + 'list_all_trial_types:noEventsFile'); + +end + +function test_list_all_trial_types_all_numeric() + + pth_bids_example = get_test_data_dir(); + + BIDS = fullfile(pth_bids_example, 'ieeg_visual'); + + %% dependencies + trial_type_list = bids.internal.list_all_trial_types(BIDS, {'visual'}, ... + 'verbose', true); + assertEqual(trial_type_list, {'1' + '2' + '3' + '4' + '5' + '6' + '7' + '8'}); + +end diff --git a/tests/tests_private/test_list_events.m b/tests/tests_private/test_list_events.m new file mode 100644 index 00000000..23e57753 --- /dev/null +++ b/tests/tests_private/test_list_events.m @@ -0,0 +1,41 @@ +function test_suite = test_list_events %#ok<*STOUT> + + close all; + + 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_list_events_basic() + + pth_bids_example = get_test_data_dir(); + + data_sets_to_test = '^ds00.*[0-9]$'; % '^ds.*[0-9]$' + examples = bids.internal.file_utils('FPList', get_test_data_dir(), 'dir', data_sets_to_test); + + for i = 1:size(examples, 1) + + BIDS = bids.layout(deblank(examples(i, :))); + + tasks = bids.query(BIDS, 'tasks'); + + for j = 1:numel(tasks) + + [bids.internal.file_utils(BIDS.pth, 'basename'), ' - ', tasks{j}]; + + [data, headers, y_labels] = bids.internal.list_events(BIDS, 'func', tasks{j}); + + bids.internal.plot_diagnostic_table(data, ... + headers, ... + y_labels, ... + [bids.internal.file_utils(BIDS.pth, 'basename'), ... + ' - ', ... + tasks{j}]); + end + + end +end diff --git a/tests/tests_private/test_parse_filename.m b/tests/tests_private/test_parse_filename.m index 0cc39c8c..8ca9ce0b 100644 --- a/tests/tests_private/test_parse_filename.m +++ b/tests/tests_private/test_parse_filename.m @@ -14,7 +14,6 @@ function test_parse_filename_warnings() fields = {}; tolerant = true; - verbose = true; filename_error = { {'_a_'; ... @@ -31,10 +30,11 @@ function test_parse_filename_warnings() for i = 1:size(filename_error, 1) for j = 1:numel(filename_error{i, 1}) - assertWarning(@()bids.internal.parse_filename(filename_error{i, 1}{j}, fields, tolerant), ... + assertWarning(@()bids.internal.parse_filename(filename_error{i, 1}{j}, fields, ... + tolerant, true), ... ['parse_filename:' filename_error{i, 2}]); - p = bids.internal.parse_filename(filename_error{i, 1}{j}, fields, tolerant); + p = bids.internal.parse_filename(filename_error{i, 1}{j}, fields, tolerant, false); assertEqual(p, struct([])); @@ -48,8 +48,7 @@ function test_parse_filename_participants() filename = 'participants.tsv'; output = bids.internal.parse_filename(filename); - expected = struct( ... - 'filename', 'participants.tsv', ... + expected = struct('filename', 'participants.tsv', ... 'suffix', 'participants', ... 'entities', struct(), ... 'ext', '.tsv', ... @@ -64,8 +63,7 @@ function test_parse_filename_prefix() filename = 'asub-16_task-rest_run-1_bold.nii'; output = bids.internal.parse_filename(filename); - expected = struct( ... - 'filename', 'asub-16_task-rest_run-1_bold.nii', ... + expected = struct('filename', 'asub-16_task-rest_run-1_bold.nii', ... 'suffix', 'bold', ... 'prefix', 'a', ... 'ext', '.nii', ... @@ -83,8 +81,7 @@ function test_parse_filename_prefix() filename = 'asub-16_task-rest_wsub-1_bold.nii'; output = bids.internal.parse_filename(filename); - expected = struct( ... - 'filename', 'asub-16_task-rest_wsub-1_bold.nii', ... + expected = struct('filename', 'asub-16_task-rest_wsub-1_bold.nii', ... 'suffix', 'bold', ... 'prefix', 'a', ... 'ext', '.nii', ... @@ -103,8 +100,7 @@ function test_parse_filename_prefix() filename = 'group-ctrl_wsub-1_bold.nii'; output = bids.internal.parse_filename(filename); - expected = struct( ... - 'filename', 'group-ctrl_wsub-1_bold.nii', ... + expected = struct('filename', 'group-ctrl_wsub-1_bold.nii', ... 'suffix', 'bold', ... 'prefix', 'group-ctrl_w', ... 'ext', '.nii', ... @@ -123,8 +119,7 @@ function test_parse_filename_basic() filename = '../sub-16/anat/sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz'; output = bids.internal.parse_filename(filename); - expected = struct( ... - 'filename', 'sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz', ... + expected = struct('filename', 'sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz', ... 'suffix', 'T1w', ... 'ext', '.nii.gz', ... 'entities', struct('sub', '16', ... @@ -147,8 +142,7 @@ function test_parse_filename_fields() fields = {'sub', 'ses', 'run', 'acq', 'ce'}; output = bids.internal.parse_filename(filename, fields); - expected = struct( ... - 'filename', 'sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz', ... + expected = struct('filename', 'sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz', ... 'suffix', 'T1w', ... 'ext', '.nii.gz', ... 'entities', struct('sub', '16', ... @@ -170,8 +164,7 @@ function test_parse_filename_wrong_template() filename = '../sub-16/anat/sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz'; - assertWarning( ... - @()bids.internal.parse_filename(filename, {'echo'}, false), ... + assertWarning(@()bids.internal.parse_filename(filename, {'echo'}, false), ... 'parse_filename:noMatchingTemplate'); end diff --git a/tests/tests_private/test_plot_diagnostic_table.m b/tests/tests_private/test_plot_diagnostic_table.m new file mode 100644 index 00000000..9185fd14 --- /dev/null +++ b/tests/tests_private/test_plot_diagnostic_table.m @@ -0,0 +1,92 @@ +function test_suite = test_plot_diagnostic_table %#ok<*STOUT> + + close all; + + 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_plot_diagnostic_table_2X2() + + headers{1}.modality = 'anat'; + headers{2}.modality = 'func'; + headers{2}.task = 'rhyme'; + + data = [0, 1; ... + 4, 10]; + + y_labels = {'sub-01 ses-02'; ... + 'sub-02 ses-02'}; + + bids.internal.plot_diagnostic_table(data, ... + headers, ... + y_labels, ... + 'ds dummy'); + +end + +function test_plot_diagnostic_table_2X3() + + headers{1}.modality = 'anat'; + headers{2}.modality = 'func'; + headers{2}.task = 'rhyme'; + headers{3}.modality = 'func'; + headers{3}.task = 'listen'; + + data = [0, 1, 3; ... + 4, 10, 5]; + + y_labels = {'sub-01 ses-02'; ... + 'sub-02 ses-02'}; + + bids.internal.plot_diagnostic_table(data, ... + headers, ... + y_labels, ... + 'ds dummy'); + +end + +function test_plot_diagnostic_table_4X4() + + headers{1}.modality = 'anat'; + headers{2}.modality = 'dwi'; + headers{3}.modality = 'func'; + headers{3}.task = 'rhyme'; + headers{4}.modality = 'func'; + headers{4}.task = 'listen'; + + data = [0, 1, 3, 0; ... + 4, 10, 5, 6; ... + 1, 2, 2, 5; ... + 1, 3, 4, 5]; + + y_labels = {'sub-01 ses-02' + 'sub-01 ses-01' + 'sub-02 ses-02' + 'sub-02 ses-01'}; + + bids.internal.plot_diagnostic_table(data, ... + headers, ... + y_labels, ... + 'ds dummy'); + +end + +function test_plot_diagnostic_table_error() + + headers{1}.modality = 'anat'; + headers{2}.modality = 'func'; + + data = 0; + + assertExceptionThrown(@()bids.internal.plot_diagnostic_table(data, ... + headers, ... + {'sub-01 ses-02'}, ... + 'ds dummy'), ... + 'plot_diagnostic_table:tableLabelsMismatch'); + +end diff --git a/tests/tests_query/test_bids_query.m b/tests/tests_query/test_bids_query.m index fbac4bd4..94ae588a 100644 --- a/tests/tests_query/test_bids_query.m +++ b/tests/tests_query/test_bids_query.m @@ -65,9 +65,7 @@ function test_query_regex_subjects_no_regex_by_default() % end - pth_bids_example = get_test_data_dir(); - - BIDS = bids.layout(fullfile(pth_bids_example, '..', 'data', 'dummy', 'raw')); + BIDS = bids.layout(fullfile(get_test_data_dir(), '..', 'data', 'synthetic')); data = bids.query(BIDS, 'subjects', 'sub', '01'); @@ -76,6 +74,7 @@ function test_query_regex_subjects_no_regex_by_default() data = bids.query(BIDS, 'subjects', 'sub', '*01'); assertEqual(numel(data), 3); + end function test_query_regex_subjects() diff --git a/tests/tests_utils/test_create_data_dict.m b/tests/tests_utils/test_create_data_dict.m new file mode 100644 index 00000000..2709e684 --- /dev/null +++ b/tests/tests_utils/test_create_data_dict.m @@ -0,0 +1,193 @@ +function test_suite = test_create_data_dict %#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_create_data_dict_basic() + + pth_bids_example = get_test_data_dir(); + + BIDS = bids.layout(fullfile(pth_bids_example, 'ds001')); + + tsv_files = bids.query(BIDS, 'data', ... + 'sub', '01', ... + 'suffix', 'events'); + + % no file written + bids.util.create_data_dict(tsv_files{1}, 'output', [], 'schema', true); + assertEqual(exist('tmp.json', 'file'), 0); + teardown(); + + % file written + data_dict = bids.util.create_data_dict(tsv_files{1}, 'output', 'tmp.json', 'schema', true); + assertEqual(exist('tmp.json', 'file'), 2); + assertEqual(data_dict.onset.Units, 's'); + teardown(); + + % do not use schema + data_dict = bids.util.create_data_dict(tsv_files{1}, 'output', [], 'schema', false); + assertEqual(data_dict.onset.Units, 'TODO'); + teardown(); + + % overwrite + bids.util.create_data_dict(tsv_files{1}, 'output', 'tmp.json', 'schema', true); + data_dict = bids.util.jsondecode('tmp.json'); + assertEqual(data_dict.onset.Units, 's'); + bids.util.create_data_dict(tsv_files{1}, 'output', 'tmp.json', 'schema', false, ... + 'force', true); + data_dict = bids.util.jsondecode('tmp.json'); + assertEqual(data_dict.onset.Units, 'TODO'); + teardown(); + +end + +function test_create_data_dict_schema() + + pth_bids_example = get_test_data_dir(); + + BIDS = bids.layout(fullfile(pth_bids_example, 'ds001')); + + tsv_files = bids.query(BIDS, 'data', ... + 'suffix', 'events'); + + schema = bids.Schema(); + schema.load_schema_metadata = true; + schema = schema.load(); + + data_dict = bids.util.create_data_dict(tsv_files{1}, 'output', 'tmp.json', 'schema', schema); + teardown(); + +end + +function test_create_data_dict_warning + + if bids.internal.is_octave + return + end + + dataset = 'ds000248'; + + schema = bids.Schema(); + schema.load_schema_metadata = true; + schema = schema.load(); + + pth_bids_example = get_test_data_dir(); + + BIDS = bids.layout(fullfile(pth_bids_example, dataset)); + + tasks = bids.query(BIDS, 'tasks'); + + tsv_files = bids.query(BIDS, 'data', ... + 'task', tasks{1}, ... + 'suffix', 'events'); + + assertWarning(@()bids.util.create_data_dict(tsv_files, ... + 'schema', schema, ... + 'level_limit', 50), ... + 'create_data_dict:modifiedLevel'); + +end + +function test_create_data_dict_several_tsv() + + %% WITH SCHEMA + % data time (sec) + % eeg_face13 0.387 + % ds003 0.480 + % eeg_cbm 0.636 + % ds101 0.963 + % genetics_ukbb 0.989 + % ieeg_visual_multimodal 1.014 + % ds005 1.060 + % ds105 1.064 + % ds102 1.281 + % ds052 1.293 + % eeg_rishikesh 1.486 + % ds008 1.498 + % ds011 1.654 + % ds114 1.667 + % ds109 1.767 + % ds002 1.855 + % ds051 1.873 + % eeg_ds000117 1.900 + % ds116 1.975 + % ds007 2.356 + % ds107 2.386 + % ds113b 2.554 + % ds009 2.732 + % ds210 2.820 + % ds110 3.263 + % ds006 3.378 + % ds108 3.897 + % ds000117 5.958 + + datasets = {'ds000248'; ... + 'ds000246'; ... + 'ieeg_visual'; ... + 'ieeg_epilepsy_ecog'; ... + 'eeg_matchingpennies'; ... + 'ds000247'; ... + 'ieeg_filtered_speech'; ... + 'eeg_face13'; ... + 'ds005'; ... + 'ds105'; ... + 'ds102'; ... + 'ds052'; ... + 'eeg_rishikesh'; ... + 'ds008'; ... + 'ds011'; ... + 'ds114'; ... + 'ds109'; ... + 'ds002'; ... + 'ds051' }; + + schema = bids.Schema(); + schema.load_schema_metadata = true; + schema = schema.load(); + + pth_bids_example = get_test_data_dir(); + + for i_dataset = 1:numel(datasets) + + dataset = datasets{i_dataset}; + + BIDS = bids.layout(fullfile(pth_bids_example, dataset)); + + tasks = bids.query(BIDS, 'tasks'); + + for i_task = 1:numel(tasks) + + tsv_files = bids.query(BIDS, 'data', ... + 'task', tasks{i_task}, ... + 'suffix', 'events'); + + data_dict = bids.util.create_data_dict(tsv_files, ... + 'output', [dataset '_' tasks{i_task} '.json'], ... + 'schema', schema, ... + 'level_limit', 50, ... + 'verbose', false); + teardown([dataset '_' tasks{i_task} '.json']); + + end + + end + +end + +function teardown(files) + if nargin < 1 + files = []; + end + if exist('tmp.json', 'file') + delete('tmp.json'); + end + if ~isempty(files) && exist(files, 'file') + delete(files); + end + if exist('modified_levels.tsv', 'file') == 2 + delete('modified_levels.tsv'); + end +end diff --git a/tests/tests_utils/test_create_participants_tsv.m b/tests/tests_utils/test_create_participants_tsv.m new file mode 100644 index 00000000..788230b9 --- /dev/null +++ b/tests/tests_utils/test_create_participants_tsv.m @@ -0,0 +1,40 @@ +function test_suite = test_create_participants_tsv %#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_create_participants_tsv_basic() + + bids_path = fullfile(get_test_data_dir(), 'asl001'); + + validate_dataset(bids_path); + + output_filename = bids.util.create_participants_tsv(bids_path, 'verbose', false); + + validate_dataset(bids_path); + + delete(output_filename); + +end + +function test_create_participants_tsv_already_exist() + + if bids.internal.is_octave() + return + end + + bids_path = fullfile(get_test_data_dir(), 'ds210'); + + output_filename = bids.util.create_participants_tsv(bids_path); + + validate_dataset(bids_path); + + assertWarning(@()bids.util.create_participants_tsv(bids_path, 'verbose', true), ... + 'create_participants_tsv:participantFileExist'); + + delete(output_filename); + +end diff --git a/tests/tests_utils/test_create_readme.m b/tests/tests_utils/test_create_readme.m new file mode 100644 index 00000000..e452c4d4 --- /dev/null +++ b/tests/tests_utils/test_create_readme.m @@ -0,0 +1,51 @@ +function test_suite = test_create_readme %#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_create_readme_basic() + + bids_path = fullfile(get_test_data_dir(), 'ds210'); + + validate_dataset(bids_path); + + bids.util.create_readme(bids_path, false, ... + 'tolerant', true, ... + 'verbose', false); + + assertEqual(exist(fullfile(bids_path, 'README.md'), 'file'), 2); + + validate_dataset(bids_path); + + delete(fullfile(bids_path, 'README.md')); + +end + +function test_create_readme_warning_already_present() + + if bids.internal.is_octave + return + end + + bids_path = fullfile(get_test_data_dir(), 'ds116'); + + assertWarning(@()bids.util.create_readme(bids_path, false, ... + 'tolerant', true, ... + 'verbose', true), ... + 'create_readme:readmeAlreadyPresent'); + +end + +function test_create_readme_warning_layout() + + foo = struct('spam', 'egg'); + + assertWarning(@()bids.util.create_readme(foo, false, ... + 'tolerant', true, ... + 'verbose', true), ... + 'create_readme:notBidsDatasetLayout'); + +end diff --git a/tests/tests_utils/test_create_scans_tsv.m b/tests/tests_utils/test_create_scans_tsv.m new file mode 100644 index 00000000..89222c22 --- /dev/null +++ b/tests/tests_utils/test_create_scans_tsv.m @@ -0,0 +1,51 @@ +function test_suite = test_create_scans_tsv %#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_create_scans_tsv_basic_no_session() + + bids_path = fullfile(get_test_data_dir(), 'asl001'); + + validate_dataset(bids_path); + + output_filenames = bids.util.create_scans_tsv(bids_path, 'verbose', false); + + assertEqual(numel(output_filenames), 1); + assertEqual(exist(fullfile(bids_path, output_filenames{1}), 'file'), 2); + content = bids.util.tsvread(fullfile(bids_path, output_filenames{1})); + assertEqual(fieldnames(content), {'filename'; 'acq_time'; 'comments'}); + + validate_dataset(bids_path); + + teardown(bids_path, output_filenames); + +end + +function test_create_scans_tsv_basic() + + bids_path = fullfile(get_test_data_dir(), 'ds000117'); + + validate_dataset(bids_path); + + output_filenames = bids.util.create_scans_tsv(bids_path, 'verbose', false); + + assertEqual(numel(output_filenames), 16); + assertEqual(exist(fullfile(bids_path, output_filenames{1}), 'file'), 2); + content = bids.util.tsvread(fullfile(bids_path, output_filenames{1})); + assertEqual(fieldnames(content), {'filename'; 'acq_time'; 'comments'}); + + validate_dataset(bids_path); + + teardown(bids_path, output_filenames); + +end + +function teardown(pth, filelist) + for i = 1:numel(filelist) + delete(fullfile(pth, filelist{i})); + end +end diff --git a/tests/tests_utils/test_create_sessions_tsv.m b/tests/tests_utils/test_create_sessions_tsv.m new file mode 100644 index 00000000..2aa8f3fb --- /dev/null +++ b/tests/tests_utils/test_create_sessions_tsv.m @@ -0,0 +1,54 @@ +function test_suite = test_create_sessions_tsv %#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_create_sessions_tsv_no_session() + + bids_path = fullfile(get_test_data_dir(), 'ds210'); + + validate_dataset(bids_path); + + output_filenames = bids.util.create_sessions_tsv(bids_path, 'verbose', false); + + assert(isempty(output_filenames)); + + validate_dataset(bids_path); + + if bids.internal.is_octave + return + end + + assertWarning(@() bids.util.create_sessions_tsv(bids_path, 'verbose', true), ... + 'create_sessions_tsv:noSessionInDataset'); + +end + +function test_create_sessions_tsv_basic() + + bids_path = fullfile(get_test_data_dir(), 'ieeg_epilepsy'); + + validate_dataset(bids_path); + + output_filenames = bids.util.create_sessions_tsv(bids_path, 'verbose', false); + + assertEqual(numel(output_filenames), 1); + assertEqual(exist(fullfile(bids_path, output_filenames{1}), 'file'), 2); + content = bids.util.tsvread(fullfile(bids_path, output_filenames{1})); + assertEqual(fieldnames(content), {'session_id'; 'acq_time'; 'comments'}); + assertEqual(content.session_id, {'ses-postimp'; 'ses-preimp'}); + + validate_dataset(bids_path); + + teardown(bids_path, output_filenames); + +end + +function teardown(pth, filelist) + for i = 1:numel(filelist) + delete(fullfile(pth, filelist{i})); + end +end diff --git a/tests/tests_utils/test_download_ds.m b/tests/tests_utils/test_download_ds.m new file mode 100644 index 00000000..c8c0cb63 --- /dev/null +++ b/tests/tests_utils/test_download_ds.m @@ -0,0 +1,23 @@ +function test_suite = test_download_ds %#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_download_ds_basic() + + bu_folder = fixture_moae(); + + pth = bids.util.download_ds('source', 'spm', ... + 'demo', 'moae', ... + 'force', true, ... + 'verbose', false, ... + 'delete_previous', true); + + bids.layout(pth); + + teardown_moae(bu_folder); + +end diff --git a/tests/tests_utils/test_plot_events.m b/tests/tests_utils/test_plot_events.m new file mode 100644 index 00000000..a0084bf2 --- /dev/null +++ b/tests/tests_utils/test_plot_events.m @@ -0,0 +1,66 @@ +function test_suite = test_plot_events %#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_plot_events_ds101() + + close all; + + data_dir = fullfile(get_test_data_dir(), 'ds001'); + + BIDS = bids.layout(data_dir); + + events_files = bids.query(BIDS, ... + 'data', ... + 'sub', '01', ... + 'task', 'balloonanalogrisktask', ... + 'suffix', 'events'); + + bids.util.plot_events(events_files); + +end + +function test_plot_events_ds101_with_model() + + data_dir = fullfile(get_test_data_dir(), 'ds001'); + + mode_file = fullfile(get_test_data_dir(), '..', ... + 'data', ... + 'model', ... + 'model-balloonanalogrisktask_smdl.json'); + + BIDS = bids.layout(data_dir); + + events_files = bids.query(BIDS, ... + 'data', ... + 'sub', '01', ... + 'task', 'balloonanalogrisktask', ... + 'suffix', 'events'); + + bids.util.plot_events(events_files, 'model_file', mode_file); + +end + +function test_plot_events_ds108() + + data_dir = fullfile(get_test_data_dir(), 'ds108'); + + BIDS = bids.layout(data_dir); + + events_files = bids.query(BIDS, ... + 'data', ... + 'sub', '01', ... + 'run', '01', ... + 'suffix', 'events'); + + include = 'Reapp_Neg_Cue'; + bids.util.plot_events(events_files, 'include', include); + + include = {'Reapp_Neg_Cue', 'Look_Neg_Cue', 'Look_Neutral_Cue'}; + bids.util.plot_events(events_files, 'include', include); + +end diff --git a/tests/tests_utils/test_tsvread.m b/tests/tests_utils/test_tsvread.m index aaa2d0a2..456d5e48 100644 --- a/tests/tests_utils/test_tsvread.m +++ b/tests/tests_utils/test_tsvread.m @@ -44,4 +44,5 @@ function test_tsvread_basic() expected.onset = ([42 126 210 294 378 462 546])'; expected.duration = repmat(42, 7, 1); expected.trial_type = repmat({'listening'}, 7, 1); + end diff --git a/tests/utils/face_rep_events.m b/tests/utils/face_rep_events.m new file mode 100644 index 00000000..01f4c6ff --- /dev/null +++ b/tests/utils/face_rep_events.m @@ -0,0 +1,10 @@ +function value = face_rep_events() + + value.onset = [2; 4; 5; 8]; + value.duration = [2; 2; 2; 2]; + value.repetition = [1; 1; 2; 2]; + value.familiarity = {'Famous face'; 'Unfamiliar face'; 'Famous face'; 'Unfamiliar face'}; + value.trial_type = {'Face'; 'Face'; 'Face'; 'Face'}; + value.response_time = [1.5; 2; 1.56; 2.1]; + +end diff --git a/tests/utils/fixture_moae.m b/tests/utils/fixture_moae.m new file mode 100644 index 00000000..0ae915d5 --- /dev/null +++ b/tests/utils/fixture_moae.m @@ -0,0 +1,7 @@ +function bu_folder = fixture_moae() + + % back up content + bu_folder = tempname; + copyfile(moae_dir(), bu_folder); + +end diff --git a/tests/utils/get_test_data_dir.m b/tests/utils/get_test_data_dir.m index 4354e9b5..22f2713b 100644 --- a/tests/utils/get_test_data_dir.m +++ b/tests/utils/get_test_data_dir.m @@ -21,4 +21,6 @@ error(msg); %#ok end + data_dir = bids.internal.file_utils(data_dir, 'cpath'); + end diff --git a/tests/utils/moae_dir.m b/tests/utils/moae_dir.m new file mode 100644 index 00000000..ece52444 --- /dev/null +++ b/tests/utils/moae_dir.m @@ -0,0 +1,6 @@ +function value = moae_dir() + value = bids.internal.file_utils(fullfile(get_test_data_dir(), ... + '..', '..', ... + 'demos', 'spm', 'moae'), ... + 'cpath'); +end diff --git a/tests/utils/participants.m b/tests/utils/participants.m new file mode 100644 index 00000000..230f84e1 --- /dev/null +++ b/tests/utils/participants.m @@ -0,0 +1,12 @@ +function value = participants() + % + + % (C) Copyright 2022 Remi Gau + + value.sex_m = [true; true; false; false; false]; + value.handedness = {'right'; 'left'; nan; 'left'; 'right'}; + value.sex = {'M'; 'M'; 'F'; 'F'; 'F'}; + value.age_gt_twenty = [true; false; true; false; false]; + value.age = [21; 18; 46; 10; nan]; + +end diff --git a/tests/utils/teardown_moae.m b/tests/utils/teardown_moae.m new file mode 100644 index 00000000..10b94e2e --- /dev/null +++ b/tests/utils/teardown_moae.m @@ -0,0 +1,9 @@ +function teardown_moae(bu_folder) + + % remove data + rmdir(moae_dir(), 's'); + + % bring backup back + copyfile(bu_folder, moae_dir()); + +end diff --git a/tests/utils/validate_dataset.m b/tests/utils/validate_dataset.m new file mode 100644 index 00000000..d290af0f --- /dev/null +++ b/tests/utils/validate_dataset.m @@ -0,0 +1,12 @@ +function validate_dataset(bids_path) + + % testing in CI with octave happens through Moxunit action + % which does not support the bids validator + if bids.internal.is_octave + return + end + + [sts, msg] = bids.validate(bids_path, '--config.ignore=99 --ignoreNiftiHeaders'); + assertEqual(sts, 0); + +end diff --git a/tests/utils/vis_motion_events.m b/tests/utils/vis_motion_events.m new file mode 100644 index 00000000..3b6a4691 --- /dev/null +++ b/tests/utils/vis_motion_events.m @@ -0,0 +1,11 @@ +function value = vis_motion_events() + % + + % (C) Copyright 2022 Remi Gau + + value.onset = [2; 4]; + value.duration = [2; 2]; + value.trial_type = {'VisMot'; 'VisStat'}; + value.intensity = [2; -4]; + +end diff --git a/tests/utils/vis_motion_to_threshold_events.m b/tests/utils/vis_motion_to_threshold_events.m new file mode 100644 index 00000000..c07f8cda --- /dev/null +++ b/tests/utils/vis_motion_to_threshold_events.m @@ -0,0 +1,11 @@ +function value = vis_motion_to_threshold_events() + % + + % (C) Copyright 2022 Remi Gau + + value.onset = [2; 4; 6; 8]; + value.duration = [2; 2; 2; 2]; + value.trial_type = {'VisMot'; 'VisStat'; 'VisMot'; 'VisStat'}; + value.to_threshold = [1; 2; -1; -2]; + +end diff --git a/tests/utils/write_test_definition_to_file.m b/tests/utils/write_test_definition_to_file.m new file mode 100644 index 00000000..923da053 --- /dev/null +++ b/tests/utils/write_test_definition_to_file.m @@ -0,0 +1,23 @@ +function write_test_definition_to_file(input, output, trans, test_name, test_type) + + if strfind(test_name, 'multi') %#ok + return + end + + test_name = strrep(test_name, 'test_', ''); + + output_dir = fullfile(pwd, 'tmp', test_type, test_name); + bids.util.mkdir(output_dir); + + input_file = fullfile(output_dir, 'input.tsv'); + bids.util.tsvwrite(input_file, input); + + input_file = fullfile(output_dir, 'output.tsv'); + bids.util.tsvwrite(input_file, output); + + trans_file = fullfile(output_dir, 'transformation.json'); + content = struct('Description', strrep(test_name, '_', ' '), ... + 'Instruction', {{trans}}); + bids.util.jsonencode(trans_file, content); + +end diff --git a/version.txt b/version.txt index b82608c0..e1847fa9 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v0.1.0 +v0.1.0dev