From 4e7b7142b4da5858dd1879532da114c0b58e0d43 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 17 Apr 2021 15:22:11 +0200 Subject: [PATCH 01/15] add basic filename creation --- +bids/+internal/camel_case.m | 28 ++++++++++++++++++++++ +bids/+util/create_filename.m | 41 ++++++++++++++++++++++++++++++++ tests/test_create_filename.m | 44 +++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 +bids/+internal/camel_case.m create mode 100644 +bids/+util/create_filename.m create mode 100644 tests/test_create_filename.m diff --git a/+bids/+internal/camel_case.m b/+bids/+internal/camel_case.m new file mode 100644 index 00000000..d23412c6 --- /dev/null +++ b/+bids/+internal/camel_case.m @@ -0,0 +1,28 @@ +function str = camel_case(str) + % + % Removes non alphanumeric characters and uppercase first letter of all + % words but the first + % + % USAGE:: + % + % str = camel_case(str) + % + % :param str: + % :type str: string + % + % :returns: + % :str: (string) returns the input with an upper case for first letter + % for all words but the first one (``camelCase``) and + % removes invalid characters (like spaces). + % + % + + % camel case: upper case for first letter for all words but the first one + spaceIdx = regexp(str, '[a-zA-Z0-9]*', 'start'); + str(spaceIdx(2:end)) = upper(str(spaceIdx(2:end))); + + % remove invalid characters + [unvalidCharacters] = regexp(str, '[^a-zA-Z0-9]'); + str(unvalidCharacters) = []; + +end diff --git a/+bids/+util/create_filename.m b/+bids/+util/create_filename.m new file mode 100644 index 00000000..98de66e4 --- /dev/null +++ b/+bids/+util/create_filename.m @@ -0,0 +1,41 @@ +function filename = create_filename(p, file) + + if nargin > 1 + p = rename_file(p, file); + end + + entities = fieldnames(p.entities); + + filename = ''; + for iEntity = 1:numel(entities) + + thisEntity = entities{iEntity}; + + if ~isempty(p.entities.(thisEntity)) + thisLabel = bids.internal.camel_case(p.entities.(thisEntity)); + filename = [filename '_' thisEntity '-' thisLabel]; %#ok + end + + end + + % remove lead '_' + filename(1) = []; + + ext = p.ext; + suffix = p.suffix; + filename = [filename '_', suffix ext]; + +end + + +function parsed_file = rename_file(p, file) + + parsed_file = bids.internal.parse_filename(file); + + entities_to_change = fieldnames(p.entities); + + for iEntity = 1:numel(entities_to_change) + parsed_file.entities.(entities_to_change{iEntity}) = p.entities.(entities_to_change{iEntity}); + end + +end \ No newline at end of file diff --git a/tests/test_create_filename.m b/tests/test_create_filename.m new file mode 100644 index 00000000..bad36eaa --- /dev/null +++ b/tests/test_create_filename.m @@ -0,0 +1,44 @@ +function test_suite = test_create_filename %#ok<*STOUT> + % + % Copyright (C) 2021 BIDS-MATLAB developers + + try % assignment of 'localfunctions' is necessary in Matlab >= 2016 + test_functions = localfunctions(); %#ok<*NASGU> + catch % no problem; early Matlab versions can use initTestSuite fine + end + initTestSuite; + +end + +function test_create_filename_basic() + + %% Create filename + p.suffix = 'bold'; + p.ext = '.nii'; + p.entities = struct(... + 'sub', '01', ... + 'ses', 'test', ... + 'task', 'face recognition', ... + 'run', '02'); + + filename = bids.util.create_filename(p); + + assertEqual(filename, 'sub-01_ses-test_task-faceRecognition_run-02_bold.nii'); + + %% Modify existing filename + p.entities = struct(... + 'sub', '02', ... + 'task', 'new task'); + + filename = bids.util.create_filename(p, fullfile(pwd,filename)); + + assertEqual(filename, 'sub-02_ses-test_task-newTask_run-02_bold.nii'); + + %% Remove entity from filename + p.entities = struct('ses', ''); + + filename = bids.util.create_filename(p, filename); + + assertEqual(filename, 'sub-02_task-newTask_run-02_bold.nii'); + +end \ No newline at end of file From 3592a2e1d04887554860d495f6423165bed156e7 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 17 Apr 2021 15:48:55 +0200 Subject: [PATCH 02/15] reorder entities if necessary --- +bids/+util/create_filename.m | 13 ++++++++++--- tests/test_create_filename.m | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/+bids/+util/create_filename.m b/+bids/+util/create_filename.m index 98de66e4..aa4fc79e 100644 --- a/+bids/+util/create_filename.m +++ b/+bids/+util/create_filename.m @@ -5,6 +5,15 @@ end entities = fieldnames(p.entities); + + % reorder entities if necessary + if isfield(p, 'entity_order') + if size(p.entity_order, 2)>1 + p.entity_order = p.entity_order'; + end + idx = ismember(entities, p.entity_order); + entities = cat(1, p.entity_order, entities(~idx)); + end filename = ''; for iEntity = 1:numel(entities) @@ -21,9 +30,7 @@ % remove lead '_' filename(1) = []; - ext = p.ext; - suffix = p.suffix; - filename = [filename '_', suffix ext]; + filename = [filename '_', p.suffix p.ext]; end diff --git a/tests/test_create_filename.m b/tests/test_create_filename.m index bad36eaa..0ec64576 100644 --- a/tests/test_create_filename.m +++ b/tests/test_create_filename.m @@ -41,4 +41,22 @@ function test_create_filename_basic() assertEqual(filename, 'sub-02_task-newTask_run-02_bold.nii'); +end + +function test_create_filename_order() + + %% Create filename + p.suffix = 'bold'; + p.ext = '.nii'; + p.entities = struct(... + 'sub', '01', ... + 'ses', 'test', ... + 'task', 'face recognition', ... + 'run', '02'); + p.entity_order = {'sub', 'run'}; + + filename = bids.util.create_filename(p); + + assertEqual(filename, 'sub-01_run-02_ses-test_task-faceRecognition_bold.nii'); + end \ No newline at end of file From 2b65e20f84f9418bcbc2be97aecef5cb7fe9dc20 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 17 Apr 2021 17:10:10 +0200 Subject: [PATCH 03/15] add possibility to create schema based names --- +bids/+internal/append_to_layout.m | 35 +------ +bids/+internal/get_metadata.m | 6 +- +bids/+schema/find_suffix_group.m | 32 ++++++ +bids/+schema/return_entities_for_suffix.m | 45 +++++++++ +bids/+util/create_filename.m | 59 ++++++++--- +bids/layout.m | 4 +- tests/test_create_filename.m | 110 ++++++++++++--------- tests/test_get_metadata.m | 4 +- tests/test_layout_derivatives.m | 2 +- tests/test_return_entities_for_suffix.m | 19 ++++ tests/test_tsvwrite.m | 28 +++--- 11 files changed, 229 insertions(+), 115 deletions(-) create mode 100644 +bids/+schema/find_suffix_group.m create mode 100644 +bids/+schema/return_entities_for_suffix.m create mode 100644 tests/test_return_entities_for_suffix.m diff --git a/+bids/+internal/append_to_layout.m b/+bids/+internal/append_to_layout.m index 86715331..3d73c4d6 100644 --- a/+bids/+internal/append_to_layout.m +++ b/+bids/+internal/append_to_layout.m @@ -26,7 +26,7 @@ % Then reparse the file using the entity-label pairs defined in the schema. p = bids.internal.parse_filename(file); - idx = find_suffix_group(modality, p.suffix, schema); + idx = bids.schema.find_suffix_group(modality, p.suffix, schema); if ~isempty(schema) @@ -61,36 +61,3 @@ end end - -function idx = find_suffix_group(modality, suffix, schema) - - idx = []; - - if isempty(schema) - return - end - - % the following loop could probably be improved with some cellfun magic - % cellfun(@(x, y) any(strcmp(x,y)), {p.type}, suffix_groups) - for i = 1:size(schema.datatypes.(modality), 1) - - this_suffix_group = schema.datatypes.(modality)(i); - - % for CI - if iscell(this_suffix_group) - this_suffix_group = this_suffix_group{1}; - end - - if any(strcmp(suffix, this_suffix_group.suffixes)) - idx = i; - break - end - - end - - if isempty(idx) - warning('findSuffix:noMatchingSuffix', ... - 'No corresponding suffix in schema for %s for datatype %s', suffix, modality); - end - -end diff --git a/+bids/+internal/get_metadata.m b/+bids/+internal/get_metadata.m index 1c0759f1..3120650a 100644 --- a/+bids/+internal/get_metadata.m +++ b/+bids/+internal/get_metadata.m @@ -61,13 +61,13 @@ entities = fieldnames(p2.entities); end - % Check if this metadata file contains - % - the same entity-label pairs + % Check if this metadata file contains + % - the same entity-label pairs % - same prefix % as its data file counterpart ismeta = true; if ~strcmp(p.suffix, p2.suffix) - ismeta = false; + ismeta = false; end for j = 1:numel(entities) if ~isfield(p.entities, entities{j}) || ... diff --git a/+bids/+schema/find_suffix_group.m b/+bids/+schema/find_suffix_group.m new file mode 100644 index 00000000..1c00574c --- /dev/null +++ b/+bids/+schema/find_suffix_group.m @@ -0,0 +1,32 @@ +function idx = find_suffix_group(modality, suffix, schema) + + idx = []; + + if isempty(schema) + return + end + + % the following loop could probably be improved with some cellfun magic + % cellfun(@(x, y) any(strcmp(x,y)), {p.type}, suffix_groups) + for i = 1:size(schema.datatypes.(modality), 1) + + this_suffix_group = schema.datatypes.(modality)(i); + + % for CI + if iscell(this_suffix_group) + this_suffix_group = this_suffix_group{1}; + end + + if any(strcmp(suffix, this_suffix_group.suffixes)) + idx = i; + break + end + + end + + if isempty(idx) + warning('findSuffix:noMatchingSuffix', ... + 'No corresponding suffix in schema for %s for datatype %s', suffix, modality); + end + +end diff --git a/+bids/+schema/return_entities_for_suffix.m b/+bids/+schema/return_entities_for_suffix.m new file mode 100644 index 00000000..3bdb5cb4 --- /dev/null +++ b/+bids/+schema/return_entities_for_suffix.m @@ -0,0 +1,45 @@ +function [entities, is_required] = return_entities_for_suffix(suffix, schema) + % + % returns the list of entities for a given suffix + + modalities = bids.schema.return_modality_groups(schema); + + for iModality = 1:numel(modalities) + + datatypes = schema.modalities.(modalities{iModality}).datatypes; + + for iDatatype = 1:numel(datatypes) + + idx = bids.schema.find_suffix_group(datatypes{iDatatype}, suffix, schema); + if ~isempty(idx) + this_datatype = datatypes{iDatatype}; + this_suffix_group = schema.datatypes.(this_datatype)(idx); + break + end + + end + + if ~isempty(idx) + is_required = check_if_required(this_suffix_group); + entities = bids.schema.return_modality_entities(this_suffix_group, schema); + break + end + + end + +end + +function is_required = check_if_required(this_suffix_group) + + entities = fieldnames(this_suffix_group.entities); + nb_entities = numel(entities); + + is_required = false(1, nb_entities); + + for i = 1:nb_entities + if strcmpi(this_suffix_group.entities.(entities{i}), 'required') + is_required(i) = true; + end + end + +end diff --git a/+bids/+util/create_filename.m b/+bids/+util/create_filename.m index aa4fc79e..751000ab 100644 --- a/+bids/+util/create_filename.m +++ b/+bids/+util/create_filename.m @@ -1,26 +1,27 @@ function filename = create_filename(p, file) + if ~isfield(p, 'suffix') + error('We need at least a suffix to create a filename.'); + end + + if ~isfield(p, 'ext') + p.ext = ''; + end + if nargin > 1 p = rename_file(p, file); end entities = fieldnames(p.entities); - - % reorder entities if necessary - if isfield(p, 'entity_order') - if size(p.entity_order, 2)>1 - p.entity_order = p.entity_order'; - end - idx = ismember(entities, p.entity_order); - entities = cat(1, p.entity_order, entities(~idx)); - end + + [p, entities] = reorder_entities(p, entities); filename = ''; for iEntity = 1:numel(entities) thisEntity = entities{iEntity}; - if ~isempty(p.entities.(thisEntity)) + if isfield(p.entities, thisEntity) && ~isempty(p.entities.(thisEntity)) thisLabel = bids.internal.camel_case(p.entities.(thisEntity)); filename = [filename '_' thisEntity '-' thisLabel]; %#ok end @@ -34,7 +35,6 @@ end - function parsed_file = rename_file(p, file) parsed_file = bids.internal.parse_filename(file); @@ -45,4 +45,39 @@ parsed_file.entities.(entities_to_change{iEntity}) = p.entities.(entities_to_change{iEntity}); end -end \ No newline at end of file +end + +function [p, entities, is_required] = reorder_entities(p, entities) + + if isfield(p, 'entity_order') + if size(p.entity_order, 2) > 1 + p.entity_order = p.entity_order'; + end + idx = ismember(entities, p.entity_order); + + entities = cat(1, p.entity_order, entities(~idx)); + is_required = false(size(entities)); + + elseif isfield(p, 'use_schema') + [p, is_required] = get_entity_order_from_schema(p); + + idx = ismember(entities, p.entity_order); + entities = cat(1, p.entity_order, entities(~idx)); + + else + + is_required = false(size(entities)); + + end + +end + +function [p, is_required] = get_entity_order_from_schema(p) + + schema = bids.schema.load_schema(p.use_schema); + [schema_entities, is_required] = bids.schema.return_entities_for_suffix(p.suffix, schema); + for i = 1:numel(schema_entities) + p.entity_order{i, 1} = schema_entities{i}; + end + +end diff --git a/+bids/layout.m b/+bids/layout.m index 920ba205..234d01d7 100644 --- a/+bids/layout.m +++ b/+bids/layout.m @@ -152,8 +152,8 @@ % NOTE: *_scans.json files can stored at the root level % and this should implemented when querying scans.tsv content + metadata subject.scans = bids.internal.file_utils('FPList', ... - subject.path, ... - ['^' subjname, '.*_scans.tsv' '$']); + subject.path, ... + ['^' subjname, '.*_scans.tsv' '$']); modality_groups = bids.schema.return_modality_groups(schema); diff --git a/tests/test_create_filename.m b/tests/test_create_filename.m index 0ec64576..7e5391d4 100644 --- a/tests/test_create_filename.m +++ b/tests/test_create_filename.m @@ -1,7 +1,7 @@ function test_suite = test_create_filename %#ok<*STOUT> % % Copyright (C) 2021 BIDS-MATLAB developers - + try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> catch % no problem; early Matlab versions can use initTestSuite fine @@ -11,52 +11,68 @@ end function test_create_filename_basic() - - %% Create filename - p.suffix = 'bold'; - p.ext = '.nii'; - p.entities = struct(... - 'sub', '01', ... - 'ses', 'test', ... - 'task', 'face recognition', ... - 'run', '02'); - - filename = bids.util.create_filename(p); - - assertEqual(filename, 'sub-01_ses-test_task-faceRecognition_run-02_bold.nii'); - - %% Modify existing filename - p.entities = struct(... - 'sub', '02', ... - 'task', 'new task'); - - filename = bids.util.create_filename(p, fullfile(pwd,filename)); - - assertEqual(filename, 'sub-02_ses-test_task-newTask_run-02_bold.nii'); - - %% Remove entity from filename - p.entities = struct('ses', ''); - - filename = bids.util.create_filename(p, filename); - - assertEqual(filename, 'sub-02_task-newTask_run-02_bold.nii'); - + + %% Create filename + p.suffix = 'bold'; + p.ext = '.nii'; + p.entities = struct( ... + 'sub', '01', ... + 'ses', 'test', ... + 'task', 'face recognition', ... + 'run', '02'); + + filename = bids.util.create_filename(p); + + assertEqual(filename, 'sub-01_ses-test_task-faceRecognition_run-02_bold.nii'); + + %% Modify existing filename + p.entities = struct( ... + 'sub', '02', ... + 'task', 'new task'); + + filename = bids.util.create_filename(p, fullfile(pwd, filename)); + + assertEqual(filename, 'sub-02_ses-test_task-newTask_run-02_bold.nii'); + + %% Remove entity from filename + p.entities = struct('ses', ''); + + filename = bids.util.create_filename(p, filename); + + assertEqual(filename, 'sub-02_task-newTask_run-02_bold.nii'); + end function test_create_filename_order() - - %% Create filename - p.suffix = 'bold'; - p.ext = '.nii'; - p.entities = struct(... - 'sub', '01', ... - 'ses', 'test', ... - 'task', 'face recognition', ... - 'run', '02'); - p.entity_order = {'sub', 'run'}; - - filename = bids.util.create_filename(p); - - assertEqual(filename, 'sub-01_run-02_ses-test_task-faceRecognition_bold.nii'); - -end \ No newline at end of file + + %% Create filename + p.suffix = 'bold'; + p.ext = '.nii'; + p.entities = struct( ... + 'sub', '01', ... + 'ses', 'test', ... + 'task', 'face recognition', ... + 'run', '02'); + p.entity_order = {'sub', 'run'}; + + filename = bids.util.create_filename(p); + + assertEqual(filename, 'sub-01_run-02_ses-test_task-faceRecognition_bold.nii'); + +end + +function test_create_filename_schema_based() + + p.suffix = 'bold'; + p.ext = '.nii'; + p.entities = struct( ... + 'run', '02', ... + 'sub', '01', ... + 'task', 'face recognition'); + p.use_schema = true; + + filename = bids.util.create_filename(p); + + assertEqual(filename, 'sub-01_task-faceRecognition_run-02_bold.nii'); + +end diff --git a/tests/test_get_metadata.m b/tests/test_get_metadata.m index e88278a2..30461841 100644 --- a/tests/test_get_metadata.m +++ b/tests/test_get_metadata.m @@ -40,7 +40,7 @@ function test_get_metadata_basic() %% test func metadata base directory metadata = bids.query(BIDS, 'metadata', 'suffix', 'bold'); -% assert(metadata.RepetitionTime == func.RepetitionTime); + % assert(metadata.RepetitionTime == func.RepetitionTime); %% test func metadata subject 01 metadata = bids.query(BIDS, 'metadata', 'sub', '01', 'suffix', 'bold'); @@ -48,7 +48,7 @@ function test_get_metadata_basic() %% test anat metadata base directory metadata = bids.query(BIDS, 'metadata', 'suffix', 'T1w'); -% assert(metadata.FlipAngle == anat.FlipAngle); + % assert(metadata.FlipAngle == anat.FlipAngle); %% test anat metadata subject 01 metadata = bids.query(BIDS, 'metadata', 'sub', '01', 'suffix', 'T1w'); diff --git a/tests/test_layout_derivatives.m b/tests/test_layout_derivatives.m index 092bddbd..a1b2bdda 100644 --- a/tests/test_layout_derivatives.m +++ b/tests/test_layout_derivatives.m @@ -25,7 +25,7 @@ function test_layout_prefix() 'prefix', 'swua'); basename = bids.internal.file_utils(data, 'basename'); assertEqual(basename, {'swuasub-01_task-balloonanalogrisktask_run-01_bold'}); - + assertEqual(bids.query(BIDS, 'prefixes'), {'swua'}); end diff --git a/tests/test_return_entities_for_suffix.m b/tests/test_return_entities_for_suffix.m new file mode 100644 index 00000000..ab25c50e --- /dev/null +++ b/tests/test_return_entities_for_suffix.m @@ -0,0 +1,19 @@ +function test_suite = test_return_entities_for_suffix %#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_return_entities_for_suffix_basic + + schema = bids.schema.load_schema(); + + entities = bids.schema.return_entities_for_suffix('bold', schema); + + expected_output = {'sub', 'ses', 'task', 'acq', 'ce', 'rec', 'dir', 'run', 'echo', 'part'}; + + assertEqual(entities, expected_output); + +end diff --git a/tests/test_tsvwrite.m b/tests/test_tsvwrite.m index b557df8d..fb7b7295 100644 --- a/tests/test_tsvwrite.m +++ b/tests/test_tsvwrite.m @@ -99,44 +99,44 @@ function test_tsvwrite_basic() % as a possible data shape for the input opf TSV write % function test_tsvwrite_row_wise_structure() -% +% % pth = fileparts(mfilename('fullpath')); -% +% % tsv_file = fullfile(pth, 'sub-01_task-STRUCTURE_events.tsv'); -% +% % logFile(1, 1).onset = 2; % logFile(1, 1).trial_type = 'motion_up'; % logFile(1, 1).duration = 1; % logFile(1, 1).speed = []; % logFile(1, 1).is_fixation = true; -% +% % logFile(2, 1).onset = NaN; % logFile(2, 1).trial_type = 'static'; % logFile(2, 1).duration = 4; % logFile(2, 1).speed = 4; % logFile(2, 1).is_fixation = 3; -% +% % bids.util.tsvwrite(tsv_file, logFile); -% +% % % read the file & % % check the extra columns of the header and some of the content -% +% % FID = fopen(tsv_file, 'r'); % C = textscan(FID, '%s%s%s%s%s', 'Delimiter', '\t', 'EndOfLine', '\n'); -% +% % % check header % assertEqual(C{4}{1}, 'speed'); -% +% % % check that empty values are entered as NaN: logFile.speed(1) % assertEqual(C{4}{2}, 'n/a'); -% +% % % check that missing fields are entered as NaN: logFile.speed(2) % assertEqual(C{4}{3}, '4'); -% +% % % check that NaN are written as : logFile.onset(2) % assertEqual(C{1}{3}, 'n/a'); % -% +% % % check values entered properly: logFile.is_fixation(2) % assertEqual(C{5}{3}, '3'); -% -% end \ No newline at end of file +% +% end From 62901111062ca120e2f3ab0ca26b66ef8d934498 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 17 Apr 2021 18:22:46 +0200 Subject: [PATCH 04/15] filename creation throws error when a required entity is missing --- +bids/+util/create_filename.m | 17 ++++++++++++++++- tests/test_create_filename.m | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/+bids/+util/create_filename.m b/+bids/+util/create_filename.m index 751000ab..712a5984 100644 --- a/+bids/+util/create_filename.m +++ b/+bids/+util/create_filename.m @@ -14,13 +14,22 @@ entities = fieldnames(p.entities); - [p, entities] = reorder_entities(p, entities); + [p, entities, is_required] = reorder_entities(p, entities); filename = ''; for iEntity = 1:numel(entities) thisEntity = entities{iEntity}; + if is_required(iEntity) && ... + (~isfield(p.entities, thisEntity) || isempty(p.entities.(thisEntity))) + errorStruct.identifier = 'bidsMatlab:requiredEntity'; + errorStruct.message = sprintf('The entity %s cannot not be empty for the suffix %s', ... + thisEntity, ... + p.suffix); + error(errorStruct); + end + if isfield(p.entities, thisEntity) && ~isempty(p.entities.(thisEntity)) thisLabel = bids.internal.camel_case(p.entities.(thisEntity)); filename = [filename '_' thisEntity '-' thisLabel]; %#ok @@ -48,6 +57,12 @@ end function [p, entities, is_required] = reorder_entities(p, entities) + % + % reorder entities by one of the following ways + % - user defined: p.entity_order + % - schema based: p.use_schema + % - order defined by entities order in p.entities + % if isfield(p, 'entity_order') if size(p.entity_order, 2) > 1 diff --git a/tests/test_create_filename.m b/tests/test_create_filename.m index 7e5391d4..2ca20397 100644 --- a/tests/test_create_filename.m +++ b/tests/test_create_filename.m @@ -76,3 +76,18 @@ function test_create_filename_schema_based() assertEqual(filename, 'sub-01_task-faceRecognition_run-02_bold.nii'); end + +function test_create_filename_schema_error() + + p.suffix = 'bold'; + p.ext = '.nii'; + p.entities = struct( ... + 'run', '02', ... + 'sub', '01'); + p.use_schema = true; + + assertExceptionThrown( ... + @()bids.util.create_filename(p), ... + 'bidsMatlab:requiredEntity'); + +end From 29adfb2510ac0cb7f49af884f120dde60d949fd2 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 23 May 2021 18:55:07 +0200 Subject: [PATCH 05/15] make create_filename more quiet --- +bids/+schema/find_suffix_group.m | 8 ++++++-- +bids/+schema/return_entities_for_suffix.m | 4 ++-- +bids/+util/create_filename.m | 22 +++++++++++++++------- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/+bids/+schema/find_suffix_group.m b/+bids/+schema/find_suffix_group.m index 1c00574c..1be607bd 100644 --- a/+bids/+schema/find_suffix_group.m +++ b/+bids/+schema/find_suffix_group.m @@ -1,6 +1,10 @@ -function idx = find_suffix_group(modality, suffix, schema) +function idx = find_suffix_group(modality, suffix, schema, quiet) idx = []; + + if nargin<4 || isempty(quiet) + quiet = true; + end if isempty(schema) return @@ -24,7 +28,7 @@ end - if isempty(idx) + if isempty(idx) && ~quiet warning('findSuffix:noMatchingSuffix', ... 'No corresponding suffix in schema for %s for datatype %s', suffix, modality); end diff --git a/+bids/+schema/return_entities_for_suffix.m b/+bids/+schema/return_entities_for_suffix.m index 3bdb5cb4..e5b7abcf 100644 --- a/+bids/+schema/return_entities_for_suffix.m +++ b/+bids/+schema/return_entities_for_suffix.m @@ -1,4 +1,4 @@ -function [entities, is_required] = return_entities_for_suffix(suffix, schema) +function [entities, is_required] = return_entities_for_suffix(suffix, schema, quiet) % % returns the list of entities for a given suffix @@ -10,7 +10,7 @@ for iDatatype = 1:numel(datatypes) - idx = bids.schema.find_suffix_group(datatypes{iDatatype}, suffix, schema); + idx = bids.schema.find_suffix_group(datatypes{iDatatype}, suffix, schema, quiet); if ~isempty(idx) this_datatype = datatypes{iDatatype}; this_suffix_group = schema.datatypes.(this_datatype)(idx); diff --git a/+bids/+util/create_filename.m b/+bids/+util/create_filename.m index 712a5984..ca40d435 100644 --- a/+bids/+util/create_filename.m +++ b/+bids/+util/create_filename.m @@ -7,7 +7,7 @@ if ~isfield(p, 'ext') p.ext = ''; end - + if nargin > 1 p = rename_file(p, file); end @@ -40,7 +40,7 @@ % remove lead '_' filename(1) = []; - filename = [filename '_', p.suffix p.ext]; + filename = [filename '_', p.suffix, p.ext]; end @@ -63,8 +63,13 @@ % - schema based: p.use_schema % - order defined by entities order in p.entities % + + if ~isfield(p, 'use_schema') + p.use_schema = true; + end + + if isfield(p, 'entity_order') && ~isempty(p.entity_order) - if isfield(p, 'entity_order') if size(p.entity_order, 2) > 1 p.entity_order = p.entity_order'; end @@ -73,8 +78,11 @@ entities = cat(1, p.entity_order, entities(~idx)); is_required = false(size(entities)); - elseif isfield(p, 'use_schema') - [p, is_required] = get_entity_order_from_schema(p); + elseif p.use_schema + + quiet = true; + + [p, is_required] = get_entity_order_from_schema(p, quiet); idx = ismember(entities, p.entity_order); entities = cat(1, p.entity_order, entities(~idx)); @@ -87,10 +95,10 @@ end -function [p, is_required] = get_entity_order_from_schema(p) +function [p, is_required] = get_entity_order_from_schema(p, quiet) schema = bids.schema.load_schema(p.use_schema); - [schema_entities, is_required] = bids.schema.return_entities_for_suffix(p.suffix, schema); + [schema_entities, is_required] = bids.schema.return_entities_for_suffix(p.suffix, schema, quiet); for i = 1:numel(schema_entities) p.entity_order{i, 1} = schema_entities{i}; end From 742bfee0b3d29cf72263ec8bdb33f4ada5090b43 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 23 May 2021 19:12:09 +0200 Subject: [PATCH 06/15] fix CI bug --- +bids/+schema/return_entities_for_suffix.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/+bids/+schema/return_entities_for_suffix.m b/+bids/+schema/return_entities_for_suffix.m index e5b7abcf..f0d5e30e 100644 --- a/+bids/+schema/return_entities_for_suffix.m +++ b/+bids/+schema/return_entities_for_suffix.m @@ -31,6 +31,11 @@ function is_required = check_if_required(this_suffix_group) + % for CI + if iscell(this_suffix_group) + this_suffix_group = this_suffix_group{1}; + end + entities = fieldnames(this_suffix_group.entities); nb_entities = numel(entities); From 361db5ee4d149ad9915fc16f599164a4e8fbf503 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 23 May 2021 19:36:37 +0200 Subject: [PATCH 07/15] add test to create derivatives name --- +bids/+schema/find_suffix_group.m | 6 ++--- +bids/+util/create_filename.m | 38 +++++++++++++++++-------------- tests/test_create_filename.m | 16 +++++++++++-- 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/+bids/+schema/find_suffix_group.m b/+bids/+schema/find_suffix_group.m index 1be607bd..4f631a3d 100644 --- a/+bids/+schema/find_suffix_group.m +++ b/+bids/+schema/find_suffix_group.m @@ -1,9 +1,9 @@ function idx = find_suffix_group(modality, suffix, schema, quiet) idx = []; - - if nargin<4 || isempty(quiet) - quiet = true; + + if nargin < 4 || isempty(quiet) + quiet = true; end if isempty(schema) diff --git a/+bids/+util/create_filename.m b/+bids/+util/create_filename.m index ca40d435..ea5a908c 100644 --- a/+bids/+util/create_filename.m +++ b/+bids/+util/create_filename.m @@ -1,17 +1,20 @@ function filename = create_filename(p, file) - if ~isfield(p, 'suffix') - error('We need at least a suffix to create a filename.'); - end + default.use_schema = true; + default.entity_order = {}; + default.ext = ''; + default.prefix = ''; + + p = bids.internal.match_structure_fields(p, default); - if ~isfield(p, 'ext') - p.ext = ''; - end - if nargin > 1 p = rename_file(p, file); end + if ~isfield(p, 'suffix') + error('We need at least a suffix to create a filename.'); + end + entities = fieldnames(p.entities); [p, entities, is_required] = reorder_entities(p, entities); @@ -40,7 +43,7 @@ % remove lead '_' filename(1) = []; - filename = [filename '_', p.suffix, p.ext]; + filename = [p.prefix, filename '_', p.suffix, p.ext]; end @@ -48,6 +51,9 @@ parsed_file = bids.internal.parse_filename(file); + parsed_file.entity_order = p.entity_order; + parsed_file.use_schema = p.use_schema; + entities_to_change = fieldnames(p.entities); for iEntity = 1:numel(entities_to_change) @@ -63,25 +69,21 @@ % - schema based: p.use_schema % - order defined by entities order in p.entities % - - if ~isfield(p, 'use_schema') - p.use_schema = true; - end - - if isfield(p, 'entity_order') && ~isempty(p.entity_order) + + if ~isempty(p.entity_order) if size(p.entity_order, 2) > 1 p.entity_order = p.entity_order'; end - idx = ismember(entities, p.entity_order); + idx = ismember(entities, p.entity_order); entities = cat(1, p.entity_order, entities(~idx)); is_required = false(size(entities)); elseif p.use_schema - + quiet = true; - + [p, is_required] = get_entity_order_from_schema(p, quiet); idx = ismember(entities, p.entity_order); @@ -89,6 +91,8 @@ else + idx = ismember(entities, p.entity_order); + entities = cat(1, p.entity_order, entities(~idx)); is_required = false(size(entities)); end diff --git a/tests/test_create_filename.m b/tests/test_create_filename.m index 2ca20397..ebc5dfd5 100644 --- a/tests/test_create_filename.m +++ b/tests/test_create_filename.m @@ -1,6 +1,4 @@ function test_suite = test_create_filename %#ok<*STOUT> - % - % Copyright (C) 2021 BIDS-MATLAB developers try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); %#ok<*NASGU> @@ -10,6 +8,20 @@ end +function test_create_filename_derivatives() + + filename = 'sub-01_ses-test_task-faceRecognition_run-02_bold.nii'; + + %% Create filename + p.entities = struct('desc', 'preproc'); + p.use_schema = false; + + filename = bids.util.create_filename(p, filename); + + assertEqual(filename, 'sub-01_ses-test_task-faceRecognition_run-02_desc-preproc_bold.nii'); + +end + function test_create_filename_basic() %% Create filename From 62bb24ff3afad1811a8b1fc77add1a8fb6eebb54 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 23 May 2021 20:09:44 +0200 Subject: [PATCH 08/15] add possibility to remove prefix when renaming --- +bids/+internal/parse_filename.m | 13 ++++++++++++- +bids/+util/create_filename.m | 8 ++++++-- tests/test_create_filename.m | 12 ++++++++++-- tests/test_return_entities_for_suffix.m | 4 +++- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/+bids/+internal/parse_filename.m b/+bids/+internal/parse_filename.m index cea3ad97..88af05bc 100644 --- a/+bids/+internal/parse_filename.m +++ b/+bids/+internal/parse_filename.m @@ -55,16 +55,27 @@ % identidy an eventual prefix to the file % and amends the sub entity accordingly p.prefix = ''; + if strfind(parts{1}, 'sub-') + tmp = regexp(parts{1}, '(sub-)', 'split'); p.prefix = tmp{1}; + if ~isempty(p.prefix) + p.entities.sub = p.entities.([p.prefix 'sub']); p.entities = rmfield(p.entities, [p.prefix 'sub']); + + % reorder entities so that the 'sub' entity stays on top + entity_order = fieldnames(p.entities); + entity_order = entity_order([end, 1:end - 1]); + p.entities = orderfields(p.entities, entity_order); + end + end - % -Extra fields can be added to the structure and ordered specifically. + % Extra fields can be added to the structure and ordered specifically. if nargin == 2 for i = 1:numel(fields) p.entities = bids.internal.add_missing_field(p.entities, fields{i}); diff --git a/+bids/+util/create_filename.m b/+bids/+util/create_filename.m index ea5a908c..7b5dff7c 100644 --- a/+bids/+util/create_filename.m +++ b/+bids/+util/create_filename.m @@ -3,8 +3,6 @@ default.use_schema = true; default.entity_order = {}; default.ext = ''; - default.prefix = ''; - p = bids.internal.match_structure_fields(p, default); if nargin > 1 @@ -15,6 +13,9 @@ error('We need at least a suffix to create a filename.'); end + default.prefix = ''; + p = bids.internal.match_structure_fields(p, default); + entities = fieldnames(p.entities); [p, entities, is_required] = reorder_entities(p, entities); @@ -53,6 +54,9 @@ parsed_file.entity_order = p.entity_order; parsed_file.use_schema = p.use_schema; + if isfield(p, 'prefix') + parsed_file.prefix = p.prefix; + end entities_to_change = fieldnames(p.entities); diff --git a/tests/test_create_filename.m b/tests/test_create_filename.m index ebc5dfd5..cd8ba96c 100644 --- a/tests/test_create_filename.m +++ b/tests/test_create_filename.m @@ -10,14 +10,22 @@ function test_create_filename_derivatives() - filename = 'sub-01_ses-test_task-faceRecognition_run-02_bold.nii'; + filename = 'wuasub-01_ses-test_task-faceRecognition_run-02_bold.nii'; - %% Create filename + % Create filename p.entities = struct('desc', 'preproc'); p.use_schema = false; filename = bids.util.create_filename(p, filename); + assertEqual(filename, 'wuasub-01_ses-test_task-faceRecognition_run-02_desc-preproc_bold.nii'); + + % Same but remove prefix + + p.prefix = ''; + + filename = bids.util.create_filename(p, filename); + assertEqual(filename, 'sub-01_ses-test_task-faceRecognition_run-02_desc-preproc_bold.nii'); end diff --git a/tests/test_return_entities_for_suffix.m b/tests/test_return_entities_for_suffix.m index ab25c50e..85389ff2 100644 --- a/tests/test_return_entities_for_suffix.m +++ b/tests/test_return_entities_for_suffix.m @@ -10,7 +10,9 @@ schema = bids.schema.load_schema(); - entities = bids.schema.return_entities_for_suffix('bold', schema); + quiet = true; + + entities = bids.schema.return_entities_for_suffix('bold', schema, quiet); expected_output = {'sub', 'ses', 'task', 'acq', 'ce', 'rec', 'dir', 'run', 'echo', 'part'}; From 8e901740e031f77f29fb222fff9764e5dda4bc9e Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 23 May 2021 20:58:12 +0200 Subject: [PATCH 09/15] create_filename also returns a possible relative filepath for the filename created --- +bids/+schema/find_suffix_datatypes.m | 25 ++++++++++++++++++++++ +bids/+schema/find_suffix_group.m | 4 ++++ +bids/+util/create_filename.m | 30 ++++++++++++++++++++++++++- tests/test_create_filename.m | 3 ++- tests/test_find_suffix_datatypes.m | 21 +++++++++++++++++++ 5 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 +bids/+schema/find_suffix_datatypes.m create mode 100644 tests/test_find_suffix_datatypes.m diff --git a/+bids/+schema/find_suffix_datatypes.m b/+bids/+schema/find_suffix_datatypes.m new file mode 100644 index 00000000..8a896756 --- /dev/null +++ b/+bids/+schema/find_suffix_datatypes.m @@ -0,0 +1,25 @@ +function datatypes = find_suffix_datatypes(suffix, schema) + % + % For a given suffix, returns all the possible datatypes that have this + % suffix. + % + + datatypes = {}; + + if isempty(schema) + return + end + + datatypes_list = fieldnames(schema.datatypes); + + for i = 1:size(datatypes_list, 1) + + suffix_list = cat(1, schema.datatypes.(datatypes_list{i}).suffixes); + + if any(ismember(suffix_list, suffix)) + datatypes{end + 1} = datatypes_list{i}; + end + + end + +end diff --git a/+bids/+schema/find_suffix_group.m b/+bids/+schema/find_suffix_group.m index 4f631a3d..57ebae9d 100644 --- a/+bids/+schema/find_suffix_group.m +++ b/+bids/+schema/find_suffix_group.m @@ -1,4 +1,8 @@ function idx = find_suffix_group(modality, suffix, schema, quiet) + % + % For a given sufffix and modality, this returns the "suffix group" this + % suffix belongs to + % idx = []; diff --git a/+bids/+util/create_filename.m b/+bids/+util/create_filename.m index 7b5dff7c..24faba59 100644 --- a/+bids/+util/create_filename.m +++ b/+bids/+util/create_filename.m @@ -1,4 +1,4 @@ -function filename = create_filename(p, file) +function [filename, pth] = create_filename(p, file) default.use_schema = true; default.entity_order = {}; @@ -46,6 +46,8 @@ filename = [p.prefix, filename '_', p.suffix, p.ext]; + pth = create_path(); + end function parsed_file = rename_file(p, file) @@ -112,3 +114,29 @@ end end + +function pth = create_path(p) + % + % Creates a relative path based on the content of the filename created + % + % If there is none, or more than one possibility for the datatype, the path will only + % be based on the sub and ses entitiy. + % + + pth = ''; + + if isfield(p.entities, 'sub') + pth = ['sub-' p.entities.sub]; + end + + if isfield(p.entities, 'ses') + pth = [pth, filesep, 'ses-', p.entities.ses]; + end + + schema = bids.schema.load_schema(); + datatypes = bids.schema.find_suffix_datatypes(p.suffix, schema); + if numel(datatypes) == 1 + pth = [pth, filesep, datatypes{1}]; + end + +end diff --git a/tests/test_create_filename.m b/tests/test_create_filename.m index cd8ba96c..806fa5fc 100644 --- a/tests/test_create_filename.m +++ b/tests/test_create_filename.m @@ -41,9 +41,10 @@ function test_create_filename_basic() 'task', 'face recognition', ... 'run', '02'); - filename = bids.util.create_filename(p); + [filename, pth] = bids.util.create_filename(p); assertEqual(filename, 'sub-01_ses-test_task-faceRecognition_run-02_bold.nii'); + assertEqual(pth, fullfile('sub-01', 'ses-test', 'func')); %% Modify existing filename p.entities = struct( ... diff --git a/tests/test_find_suffix_datatypes.m b/tests/test_find_suffix_datatypes.m new file mode 100644 index 00000000..e461879a --- /dev/null +++ b/tests/test_find_suffix_datatypes.m @@ -0,0 +1,21 @@ +function test_suite = test_find_suffix_datatypes %#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_find_suffix_datatypes_basic + + schema = bids.schema.load_schema(); + + suffix = 'bold'; + + datatypes = bids.schema.find_suffix_datatypes(suffix, schema); + + expected_output = {'func'}; + + assertEqual(datatypes, expected_output); + +end From 34bec69bf7b251e0fab900c2faf049a200e9e1dc Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 23 May 2021 21:29:19 +0200 Subject: [PATCH 10/15] update help section --- +bids/+util/create_filename.m | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/+bids/+util/create_filename.m b/+bids/+util/create_filename.m index 24faba59..74a7155a 100644 --- a/+bids/+util/create_filename.m +++ b/+bids/+util/create_filename.m @@ -1,4 +1,37 @@ function [filename, pth] = create_filename(p, file) + % + % Creates a BIDS compatible filename and can be used to create new names to rename files + % + % USAGE:: + % + % [filename, pth] = create_filename(p) + % + % :param p: specification of the filename to create, very similar to the output of + % ``bids.internal.parse_filename`` + % :type p: structure + % + % Content of ``p``: + % + % - ``p.suffix`` - required + % - ``p.ext`` - extension (default: ``p.ext = ''``) + % - ``p.entities`` - structure listing the entity-label pairs to compose the filename + % - ``p.prefix`` - prefex to prepend (default: ``p.prefix = ''``) + % - ``p.use_schema`` - bollean to check required entities for a given suffix, + % and reorder entities according to the BIDS schema. + % - ``p.entity_order`` - user specified order in which to arranges the entities + % in the filename. Overrides ``p.use_schema``. + % + % If no entity order is specified and the filename creation is not based on the BIDS + % schema, then the filename will be created by concatenating the entity-label pairs + % found in the content of ``p.entities``. + % + % USAGE:: + % + % [filename, pth] = create_filename(p, file) + % + % :param file: file whose name has to be modified by the content of ``p``. + % :type file: string + % default.use_schema = true; default.entity_order = {}; @@ -46,7 +79,7 @@ filename = [p.prefix, filename '_', p.suffix, p.ext]; - pth = create_path(); + pth = create_path(p); end From 94e47190a0c8b8734be6dac5593c93a7dacf89d9 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 24 May 2021 09:06:27 +0200 Subject: [PATCH 11/15] add function to handle JSON content of derivatives --- +bids/derivatives_json.m | 78 ++++++++++++++++++++++++ tests/test_derivatives_json.m | 111 ++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 +bids/derivatives_json.m create mode 100644 tests/test_derivatives_json.m diff --git a/+bids/derivatives_json.m b/+bids/derivatives_json.m new file mode 100644 index 00000000..5faa4b8f --- /dev/null +++ b/+bids/derivatives_json.m @@ -0,0 +1,78 @@ +function json = derivatives_json(derivative_filename, force) + % + % Creates dummy content for a given BIDS derivative file. + % + % %% Common + % Description RECOMMENDED + % Sources OPTIONAL + % RawSources OPTIONAL + % SpatialReference REQUIRED if no space entity, or if non standard space RECOMMENDED otherwise + % + % %% preprocessed + % SkullStripped REQUIRED for preprocessed data + % Resolution REQUIRED if "res" entity + % Density REQUIRED if "den" entity + % + % %% Mask + % RawSources REQUIRED + % Type RECOMMENDED (Brain, Lesion, Face, ROI) + % Atlas REQUIRED if "label" entity + % Resolution REQUIRED if "res" entity + % Density REQUIRED if "den" entity + % + % %% Segmentation + % Manual OPTIONAL + % Atlas OPTIONAL + % Resolution REQUIRED if "res" entity + % Density REQUIRED if "den" entity + + if nargin < 2 + force = false; + end + + p = bids.internal.parse_filename(derivative_filename); + + json = struct('filename', '', 'content', ''); + + if force || ... + any(ismember(fieldnames(p.entities), {'desc', 'res', 'label', 'den', 'space'})) || ... + any(ismember(p.suffix, {'mask', 'dseg', 'probseg'})) + + content = struct('Description', 'RECOMMENDED'); + + content.Sources = {{'OPTIONAL'}}; + content.RawSources = {{'OPTIONAL'}}; + content.SpatialReference = {{ ['REQUIRED if no space entity', ... + 'or if non standard space RECOMMENDED otherwise'] }}; + + %% entity related content + if any(ismember(fieldnames(p.entities), 'res')) + content.Resolution = {{ struct(p.entities.res, 'REQUIRED if "res" entity') }}; + end + + if any(ismember(fieldnames(p.entities), 'den')) + content.Density = {{ struct(p.entities.den, 'REQUIRED if "den" entity') }}; + end + + %% suffix related content + if any(ismember(p.suffix, {'dseg', 'probseg'})) + content.Manual = {{'OPTIONAL'}}; + content.Atlas = {{'OPTIONAL'}}; + end + + if any(ismember(p.suffix, {'mask'})) + content.RawSources = {{'REQUIRED'}}; + content.Atlas = {{'OPTIONAL'}}; + content.Type = {{'OPTIONAL'}}; + end + + % TODO + % preprocessed + % SkullStripped REQUIRED for preprocessed data + + json.content = content; + json.filename = strrep(derivative_filename, p.ext, '.json'); + + end + +end diff --git a/tests/test_derivatives_json.m b/tests/test_derivatives_json.m new file mode 100644 index 00000000..4e529b55 --- /dev/null +++ b/tests/test_derivatives_json.m @@ -0,0 +1,111 @@ +function test_suite = test_derivatives_json %#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_derivatives_json_basic() + + %% not a derivative file + filename = 'sub-01_ses-test_task-faceRecognition_run-02_bold.nii'; + + json = bids.derivatives_json(filename); + + expected.filename = ''; + expected.content = ''; + + assertEqual(json, expected); + +end + +function test_derivatives_json_force() + + %% force to create default content + filename = 'sub-01_task-faceRecognition_bold.nii'; + force = true; + + json = bids.derivatives_json(filename, force); + + content = struct('Description', 'RECOMMENDED'); + content.Sources = {{'OPTIONAL'}}; + content.RawSources = {{'OPTIONAL'}}; + content.SpatialReference = {{ ['REQUIRED if no space entity', ... + 'or if non standard space RECOMMENDED otherwise'] }}; + + expected.content = content; + expected.filename = 'sub-01_task-faceRecognition_bold.json'; + + assertEqual(json, expected); + +end + +function test_derivatives_json_preproc() + + filename = 'sub-01_task-faceRecognition_res-hi_den-lo_desc-preproc_bold.nii.gz'; + + json = bids.derivatives_json(filename); + + content = struct('Description', 'RECOMMENDED'); + content.Sources = {{'OPTIONAL'}}; + content.RawSources = {{'OPTIONAL'}}; + content.SpatialReference = {{ ['REQUIRED if no space entity', ... + 'or if non standard space RECOMMENDED otherwise'] }}; + + content.Resolution = {{ struct('hi', 'REQUIRED if "res" entity') }}; + content.Density = {{ struct('lo', 'REQUIRED if "den" entity') }}; + + expected.content = content; + expected.filename = 'sub-01_task-faceRecognition_res-hi_den-lo_desc-preproc_bold.json'; + + assertEqual(json.filename, expected.filename); + assertEqual(json.content, expected.content); + +end + +function test_derivatives_json_segmentation() + + filename = 'sub-01_desc-T1w_dseg.nii.gz'; + + json = bids.derivatives_json(filename); + + content = struct('Description', 'RECOMMENDED'); + content.Sources = {{'OPTIONAL'}}; + content.RawSources = {{'OPTIONAL'}}; + content.SpatialReference = {{ ['REQUIRED if no space entity', ... + 'or if non standard space RECOMMENDED otherwise'] }}; + content.Manual = {{'OPTIONAL'}}; + content.Atlas = {{'OPTIONAL'}}; + + expected.content = content; + expected.filename = 'sub-01_desc-T1w_dseg.json'; + + assertEqual(json.filename, expected.filename); + assertEqual(json.content, expected.content); + +end + +function test_derivatives_json_mask() + + filename = 'sub-01_mask.nii.gz'; + + json = bids.derivatives_json(filename); + + content = struct('Description', 'RECOMMENDED'); + content.Sources = {{'OPTIONAL'}}; + content.RawSources = {{'REQUIRED'}}; + content.SpatialReference = {{ ['REQUIRED if no space entity', ... + 'or if non standard space RECOMMENDED otherwise'] }}; + content.Atlas = {{'OPTIONAL'}}; + content.Type = {{'OPTIONAL'}}; + + expected.content = content; + expected.filename = 'sub-01_mask.json'; + + assertEqual(json.filename, expected.filename); + assertEqual(json.content, expected.content); + +end From c1e8016cea1ab399e8336e297846d3254f923e5c Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 24 May 2021 09:07:02 +0200 Subject: [PATCH 12/15] extract create path function --- +bids/+util/create_filename.m | 32 ++++---------------------------- +bids/create_path.m | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 28 deletions(-) create mode 100644 +bids/create_path.m diff --git a/+bids/+util/create_filename.m b/+bids/+util/create_filename.m index 74a7155a..6d36f7f0 100644 --- a/+bids/+util/create_filename.m +++ b/+bids/+util/create_filename.m @@ -1,4 +1,4 @@ -function [filename, pth] = create_filename(p, file) +function [filename, pth, json] = create_filename(p, file) % % Creates a BIDS compatible filename and can be used to create new names to rename files % @@ -79,7 +79,9 @@ filename = [p.prefix, filename '_', p.suffix, p.ext]; - pth = create_path(p); + pth = bids.create_path(filename); + + json = bids.derivatives_json(filename); end @@ -147,29 +149,3 @@ end end - -function pth = create_path(p) - % - % Creates a relative path based on the content of the filename created - % - % If there is none, or more than one possibility for the datatype, the path will only - % be based on the sub and ses entitiy. - % - - pth = ''; - - if isfield(p.entities, 'sub') - pth = ['sub-' p.entities.sub]; - end - - if isfield(p.entities, 'ses') - pth = [pth, filesep, 'ses-', p.entities.ses]; - end - - schema = bids.schema.load_schema(); - datatypes = bids.schema.find_suffix_datatypes(p.suffix, schema); - if numel(datatypes) == 1 - pth = [pth, filesep, datatypes{1}]; - end - -end diff --git a/+bids/create_path.m b/+bids/create_path.m new file mode 100644 index 00000000..3711a43b --- /dev/null +++ b/+bids/create_path.m @@ -0,0 +1,27 @@ +function pth = create_path(filename) + % + % Creates a relative path based on the content of a BIDS filename. + % + % If there is none, or more than one possibility for the datatype, the path will only + % be based on the sub and ses entitiy. + % + + pth = ''; + + p = bids.internal.parse_filename(filename); + + if isfield(p.entities, 'sub') + pth = ['sub-' p.entities.sub]; + end + + if isfield(p.entities, 'ses') + pth = [pth, filesep, 'ses-', p.entities.ses]; + end + + schema = bids.schema.load_schema(); + datatypes = bids.schema.find_suffix_datatypes(p.suffix, schema); + if numel(datatypes) == 1 + pth = [pth, filesep, datatypes{1}]; + end + +end From 2f6776dd16ec1a7c2f1526792024c2062ce81bb2 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 24 May 2021 09:14:47 +0200 Subject: [PATCH 13/15] make create_filename a basic function of the package --- +bids/{+util => }/create_filename.m | 0 tests/test_create_filename.m | 16 ++++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) rename +bids/{+util => }/create_filename.m (100%) diff --git a/+bids/+util/create_filename.m b/+bids/create_filename.m similarity index 100% rename from +bids/+util/create_filename.m rename to +bids/create_filename.m diff --git a/tests/test_create_filename.m b/tests/test_create_filename.m index 806fa5fc..af177dff 100644 --- a/tests/test_create_filename.m +++ b/tests/test_create_filename.m @@ -16,7 +16,7 @@ function test_create_filename_derivatives() p.entities = struct('desc', 'preproc'); p.use_schema = false; - filename = bids.util.create_filename(p, filename); + filename = bids.create_filename(p, filename); assertEqual(filename, 'wuasub-01_ses-test_task-faceRecognition_run-02_desc-preproc_bold.nii'); @@ -24,7 +24,7 @@ function test_create_filename_derivatives() p.prefix = ''; - filename = bids.util.create_filename(p, filename); + filename = bids.create_filename(p, filename); assertEqual(filename, 'sub-01_ses-test_task-faceRecognition_run-02_desc-preproc_bold.nii'); @@ -41,7 +41,7 @@ function test_create_filename_basic() 'task', 'face recognition', ... 'run', '02'); - [filename, pth] = bids.util.create_filename(p); + [filename, pth] = bids.create_filename(p); assertEqual(filename, 'sub-01_ses-test_task-faceRecognition_run-02_bold.nii'); assertEqual(pth, fullfile('sub-01', 'ses-test', 'func')); @@ -51,14 +51,14 @@ function test_create_filename_basic() 'sub', '02', ... 'task', 'new task'); - filename = bids.util.create_filename(p, fullfile(pwd, filename)); + filename = bids.create_filename(p, fullfile(pwd, filename)); assertEqual(filename, 'sub-02_ses-test_task-newTask_run-02_bold.nii'); %% Remove entity from filename p.entities = struct('ses', ''); - filename = bids.util.create_filename(p, filename); + filename = bids.create_filename(p, filename); assertEqual(filename, 'sub-02_task-newTask_run-02_bold.nii'); @@ -76,7 +76,7 @@ function test_create_filename_order() 'run', '02'); p.entity_order = {'sub', 'run'}; - filename = bids.util.create_filename(p); + filename = bids.create_filename(p); assertEqual(filename, 'sub-01_run-02_ses-test_task-faceRecognition_bold.nii'); @@ -92,7 +92,7 @@ function test_create_filename_schema_based() 'task', 'face recognition'); p.use_schema = true; - filename = bids.util.create_filename(p); + filename = bids.create_filename(p); assertEqual(filename, 'sub-01_task-faceRecognition_run-02_bold.nii'); @@ -108,7 +108,7 @@ function test_create_filename_schema_error() p.use_schema = true; assertExceptionThrown( ... - @()bids.util.create_filename(p), ... + @()bids.create_filename(p), ... 'bidsMatlab:requiredEntity'); end From 47cfdcedfd4183a38a4d8b167e09032d05a336d3 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 24 May 2021 09:31:17 +0200 Subject: [PATCH 14/15] add test to create path --- tests/test_create_path.m | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/test_create_path.m diff --git a/tests/test_create_path.m b/tests/test_create_path.m new file mode 100644 index 00000000..27190414 --- /dev/null +++ b/tests/test_create_path.m @@ -0,0 +1,26 @@ +function test_suite = test_create_path %#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_path_basic() + + filename = 'sub-01_ses-test_bold.nii'; + path = bids.create_path(filename); + assertEqual(path, fullfile('sub-01', 'ses-test', 'func')); + + % several modality possiblities for events + filename = 'sub-01_ses-test_task-test_events.tsv'; + path = bids.create_path(filename); + assertEqual(path, fullfile('sub-01', 'ses-test')); + + % filename = 'participants.tsv'; + % path = bids.create_path(filename); + % assertEqual(path, ''); + +end From 1435c73ae2fa4b21b419e86c2bf262131563189a Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 24 May 2021 10:42:10 +0200 Subject: [PATCH 15/15] fix CI issues --- +bids/+schema/find_suffix_datatypes.m | 10 +++++++++- +bids/+schema/find_suffix_group.m | 2 ++ +bids/+schema/return_entities_for_suffix.m | 2 ++ +bids/create_filename.m | 6 ++++-- +bids/create_path.m | 1 + 5 files changed, 18 insertions(+), 3 deletions(-) diff --git a/+bids/+schema/find_suffix_datatypes.m b/+bids/+schema/find_suffix_datatypes.m index 8a896756..72b02afe 100644 --- a/+bids/+schema/find_suffix_datatypes.m +++ b/+bids/+schema/find_suffix_datatypes.m @@ -3,6 +3,8 @@ % For a given suffix, returns all the possible datatypes that have this % suffix. % + % + % (C) Copyright 2021 BIDS-MATLAB developers datatypes = {}; @@ -14,7 +16,13 @@ for i = 1:size(datatypes_list, 1) - suffix_list = cat(1, schema.datatypes.(datatypes_list{i}).suffixes); + this_datatype = schema.datatypes.(datatypes_list{i}); + % for CI + if iscell(this_datatype) + this_datatype = this_datatype{1}; + end + + suffix_list = cat(1, this_datatype.suffixes); if any(ismember(suffix_list, suffix)) datatypes{end + 1} = datatypes_list{i}; diff --git a/+bids/+schema/find_suffix_group.m b/+bids/+schema/find_suffix_group.m index 57ebae9d..643eb41f 100644 --- a/+bids/+schema/find_suffix_group.m +++ b/+bids/+schema/find_suffix_group.m @@ -3,6 +3,8 @@ % For a given sufffix and modality, this returns the "suffix group" this % suffix belongs to % + % + % (C) Copyright 2021 BIDS-MATLAB developers idx = []; diff --git a/+bids/+schema/return_entities_for_suffix.m b/+bids/+schema/return_entities_for_suffix.m index f0d5e30e..f985d34e 100644 --- a/+bids/+schema/return_entities_for_suffix.m +++ b/+bids/+schema/return_entities_for_suffix.m @@ -1,6 +1,8 @@ function [entities, is_required] = return_entities_for_suffix(suffix, schema, quiet) % % returns the list of entities for a given suffix + % + % (C) Copyright 2021 BIDS-MATLAB developers modalities = bids.schema.return_modality_groups(schema); diff --git a/+bids/create_filename.m b/+bids/create_filename.m index 6d36f7f0..826ab7e9 100644 --- a/+bids/create_filename.m +++ b/+bids/create_filename.m @@ -4,7 +4,7 @@ % % USAGE:: % - % [filename, pth] = create_filename(p) + % [filename, pth, json] = bids.create_filename(p) % % :param p: specification of the filename to create, very similar to the output of % ``bids.internal.parse_filename`` @@ -27,11 +27,13 @@ % % USAGE:: % - % [filename, pth] = create_filename(p, file) + % [filename, pth, json] = bids.create_filename(p, file) % % :param file: file whose name has to be modified by the content of ``p``. % :type file: string % + % + % (C) Copyright 2021 BIDS-MATLAB developers default.use_schema = true; default.entity_order = {}; diff --git a/+bids/create_path.m b/+bids/create_path.m index 3711a43b..6de39076 100644 --- a/+bids/create_path.m +++ b/+bids/create_path.m @@ -5,6 +5,7 @@ % If there is none, or more than one possibility for the datatype, the path will only % be based on the sub and ses entitiy. % + % (C) Copyright 2021 BIDS-MATLAB developers pth = '';