From 898ee1c690e27ad2b91855f64daf62ebcd21a121 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 16 Jan 2021 12:16:10 +0100 Subject: [PATCH 01/54] create test for load_schema --- tests/schema/base.json | 3 +++ tests/schema/subfolder_1/sub.json | 3 +++ tests/schema/subfolder_2/sub.txt | 0 tests/schema/subfolder_2/subfolder_3/sub.json | 3 +++ tests/schema/subfolder_4/sub.txt | 0 tests/test_load_schema.m | 13 +++++++++++++ 6 files changed, 22 insertions(+) create mode 100644 tests/schema/base.json create mode 100644 tests/schema/subfolder_1/sub.json create mode 100644 tests/schema/subfolder_2/sub.txt create mode 100644 tests/schema/subfolder_2/subfolder_3/sub.json create mode 100644 tests/schema/subfolder_4/sub.txt create mode 100644 tests/test_load_schema.m diff --git a/tests/schema/base.json b/tests/schema/base.json new file mode 100644 index 00000000..73cd85a8 --- /dev/null +++ b/tests/schema/base.json @@ -0,0 +1,3 @@ +{ + "test": 1 +} \ No newline at end of file diff --git a/tests/schema/subfolder_1/sub.json b/tests/schema/subfolder_1/sub.json new file mode 100644 index 00000000..73cd85a8 --- /dev/null +++ b/tests/schema/subfolder_1/sub.json @@ -0,0 +1,3 @@ +{ + "test": 1 +} \ No newline at end of file diff --git a/tests/schema/subfolder_2/sub.txt b/tests/schema/subfolder_2/sub.txt new file mode 100644 index 00000000..e69de29b diff --git a/tests/schema/subfolder_2/subfolder_3/sub.json b/tests/schema/subfolder_2/subfolder_3/sub.json new file mode 100644 index 00000000..73cd85a8 --- /dev/null +++ b/tests/schema/subfolder_2/subfolder_3/sub.json @@ -0,0 +1,3 @@ +{ + "test": 1 +} \ No newline at end of file diff --git a/tests/schema/subfolder_4/sub.txt b/tests/schema/subfolder_4/sub.txt new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_load_schema.m b/tests/test_load_schema.m new file mode 100644 index 00000000..ace78b0a --- /dev/null +++ b/tests/test_load_schema.m @@ -0,0 +1,13 @@ +function test_load_schema() + + SCHEMA_DIR = fullfile(fileparts(mfilename('fullpath')), 'schema'); + + schema = bids.internal.load_schema(SCHEMA_DIR); + + assert(isfield(schema, 'base')); + assert(isfield(schema, 'subfolder_1')); + assert(isfield(schema.subfolder_1, 'sub')); +% assert(isfield(schema.subfolder_2, 'subfolder_3')); + assert(~isfield(schema, 'subfolder_4')); + +end \ No newline at end of file From 0e084104f3d71c29cc5a62fef4f3664d2b375d78 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 16 Jan 2021 12:20:30 +0100 Subject: [PATCH 02/54] add function to load schema --- +bids/+internal/load_schema.m | 63 +++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 +bids/+internal/load_schema.m diff --git a/+bids/+internal/load_schema.m b/+bids/+internal/load_schema.m new file mode 100644 index 00000000..3b9d56c2 --- /dev/null +++ b/+bids/+internal/load_schema.m @@ -0,0 +1,63 @@ +function schema = load_schema(SCHEMA_DIR) + % Loads a json schema by recursively looking through a folder structure. + % + % The nesting of the output structure reflects a combination of the folder structure and + % any eventual nesting within each json. + % + % + % Copyright (C) 2021--, BIDS-MATLAB developers + + % TODO: + % - folders that do not contain json files themselves but contain + % subfolders that do, are not reflected in the output structure (they are + % skipped). This can lead to "name conflicts". See "silenced" unit tests + % for more info. + + if nargin < 1 + SCHEMA_DIR = fullfile(fileparts(mfilename('fullpath')), '..', '..', 'schema'); + end + + schema = struct(); + + [json_file_list, dirs] = bids.internal.file_utils('FPList', SCHEMA_DIR, '^*.json$'); + + schema = append_json_content_to_structure(schema, json_file_list); + + schema = inspect_subdir(schema, dirs); + +end + +function structure = append_json_content_to_structure(structure, json_file_list) + + for iFile = 1:size(json_file_list, 1) + + file = deblank(json_file_list(iFile, :)); + + field_name = bids.internal.file_utils(file, 'basename'); + + structure.(field_name) = bids.util.jsondecode(file); + end + +end + +function structure = inspect_subdir(structure, subdir_list) + % recursively inspects subdirectory for json files and reflects folder + % hierarchy in the output structure. + + for iDir = 1:size(subdir_list, 1) + + directory = deblank(subdir_list(iDir, :)); + + [json_file_list, dirs] = bids.internal.file_utils('FPList', directory, '^*.json$'); + + if ~isempty(json_file_list) + field_name = bids.internal.file_utils(directory, 'basename'); + structure.(field_name) = struct(); + structure.(field_name) = append_json_content_to_structure(structure.(field_name), json_file_list); + end + + structure = inspect_subdir(structure, dirs); + + end + +end From 9a20988d1080984ca20e68e7df987d39edf41bdd Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 6 Feb 2021 18:21:56 +0100 Subject: [PATCH 03/54] fix conflict --- +bids/layout.m | 43 +++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/+bids/layout.m b/+bids/layout.m index 872fde01..66318485 100644 --- a/+bids/layout.m +++ b/+bids/layout.m @@ -190,16 +190,40 @@ function tolerant_message(tolerant, msg) subject.ieeg = struct([]); % iEEG data subject.pet = struct([]); % PET imaging data - subject = parse_anat(subject); - subject = parse_func(subject); - subject = parse_fmap(subject); - subject = parse_eeg(subject); - subject = parse_meg(subject); - subject = parse_beh(subject); - subject = parse_dwi(subject); - subject = parse_perf(subject); + % use BIDS schema to organizing parsing of subject data + schema = bids.internal.load_schema(); + modalities = fieldnames(schema.modalities); + + for iModality = 1:numel(modalities) + + datatypes = schema.modalities.(modalities{iModality}).datatypes; + + for iDatatype = 1:numel(datatypes) + switch datatypes{iDatatype} + case 'anat' + subject = parse_anat(subject); + case 'beh' + subject = parse_beh(subject); + case 'dwi' + subject = parse_dwi(subject); + case 'eeg' + subject = parse_eeg(subject); + case 'fmap' + subject = parse_fmap(subject); + case 'func' + subject = parse_func(subject); + case 'ieeg' + subject = parse_ieeg(subject); + case 'meg' + subject = parse_meg(subject); + case 'perf' + end + end + + end + + % not covered by schema... yet subject = parse_pet(subject); - subject = parse_ieeg(subject); end @@ -241,7 +265,6 @@ function tolerant_message(tolerant, msg) for i = 1:numel(file_list) subject = append_to_structure(file_list{i}, entities, subject, 'func'); - subject.func(end).meta = struct([]); % ? end From fce12de142c4f63f25cbca70c28638d527ec332a Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 16 Jan 2021 12:57:17 +0100 Subject: [PATCH 04/54] update load_schema test --- tests/test_load_schema.m | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/test_load_schema.m b/tests/test_load_schema.m index ace78b0a..18355b2e 100644 --- a/tests/test_load_schema.m +++ b/tests/test_load_schema.m @@ -1,13 +1,14 @@ function test_load_schema() - - SCHEMA_DIR = fullfile(fileparts(mfilename('fullpath')), 'schema'); - - schema = bids.internal.load_schema(SCHEMA_DIR); - - assert(isfield(schema, 'base')); - assert(isfield(schema, 'subfolder_1')); - assert(isfield(schema.subfolder_1, 'sub')); -% assert(isfield(schema.subfolder_2, 'subfolder_3')); - assert(~isfield(schema, 'subfolder_4')); - -end \ No newline at end of file + + SCHEMA_DIR = fullfile(fileparts(mfilename('fullpath')), 'schema'); + + schema = bids.internal.load_schema(SCHEMA_DIR); + + assert(isfield(schema, 'base')); + assert(isfield(schema, 'subfolder_1')); + assert(isfield(schema.subfolder_1, 'sub')); + % assert(isfield(schema.subfolder_2, 'subfolder_3')); + % assert(isfield(schema.subfolder_2.subfolder_3, 'sub')); + assert(~isfield(schema, 'subfolder_4')); + +end From 4eec143df348d4d71f6fa232a75cd4b22382368b Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 16 Jan 2021 14:32:58 +0100 Subject: [PATCH 05/54] add functions to deal with schema extensions, suffixes and entities --- +bids/+internal/return_datatype_entities.m | 5 +++++ +bids/+internal/return_datatype_extensions.m | 14 +++++++++++++ +bids/+internal/return_datatype_suffixes.m | 12 +++++++++++ tests/test_return_datatype_entities.m | 21 ++++++++++++++++++++ tests/test_return_datatype_extensions.m | 8 ++++++++ tests/test_return_datatype_suffixes.m | 8 ++++++++ 6 files changed, 68 insertions(+) create mode 100644 +bids/+internal/return_datatype_entities.m create mode 100644 +bids/+internal/return_datatype_extensions.m create mode 100644 +bids/+internal/return_datatype_suffixes.m create mode 100644 tests/test_return_datatype_entities.m create mode 100644 tests/test_return_datatype_extensions.m create mode 100644 tests/test_return_datatype_suffixes.m diff --git a/+bids/+internal/return_datatype_entities.m b/+bids/+internal/return_datatype_entities.m new file mode 100644 index 00000000..3c0b7882 --- /dev/null +++ b/+bids/+internal/return_datatype_entities.m @@ -0,0 +1,5 @@ +function entities = return_datatype_entities(datatype) + + entities = fieldnames(datatype.entities); + +end diff --git a/+bids/+internal/return_datatype_extensions.m b/+bids/+internal/return_datatype_extensions.m new file mode 100644 index 00000000..55a51bcd --- /dev/null +++ b/+bids/+internal/return_datatype_extensions.m @@ -0,0 +1,14 @@ +function extensions = return_datatype_extensions(datatype) + + extensions = '\\('; + + for iExt = 1:numel(datatype.extensions) + if ~strcmp(datatype.extensions{iExt}, '.json') + extensions = [extensions, datatype.extensions{iExt}, '|']; %#ok + end + end + + % Replace final "|" by a ")" + extensions(end) = ')'; + +end diff --git a/+bids/+internal/return_datatype_suffixes.m b/+bids/+internal/return_datatype_suffixes.m new file mode 100644 index 00000000..0f64954e --- /dev/null +++ b/+bids/+internal/return_datatype_suffixes.m @@ -0,0 +1,12 @@ +function suffixes = return_datatype_suffixes(datatype) + + suffixes = '\\_('; + + for iExt = 1:numel(datatype.suffixes) + suffixes = [suffixes, datatype.suffixes{iExt}, '|']; %#ok + end + + % Replace final "|" by a ")" + suffixes(end) = ')'; + +end diff --git a/tests/test_return_datatype_entities.m b/tests/test_return_datatype_entities.m new file mode 100644 index 00000000..e9e588d3 --- /dev/null +++ b/tests/test_return_datatype_entities.m @@ -0,0 +1,21 @@ +function test_return_datatype_entities + + schema = bids.internal.load_schema(); + + entities = bids.internal.return_datatype_entities(schema.datatypes.func(1)); + + expected_output = { + 'sub' + 'ses' + 'task' + 'acq' + 'ce' + 'rec' + 'dir' + 'run' + 'echo' + 'part'}; + + assert(isequal(entities, expected_output)); + +end diff --git a/tests/test_return_datatype_extensions.m b/tests/test_return_datatype_extensions.m new file mode 100644 index 00000000..4b0be48b --- /dev/null +++ b/tests/test_return_datatype_extensions.m @@ -0,0 +1,8 @@ +function test_return_datatype_extensions + + schema = bids.internal.load_schema(); + + extensions = bids.internal.return_datatype_extensions(schema.datatypes.func(1)); + assert(isequal(extensions, '\\(.nii.gz|.nii)')); + +end diff --git a/tests/test_return_datatype_suffixes.m b/tests/test_return_datatype_suffixes.m new file mode 100644 index 00000000..36236ef1 --- /dev/null +++ b/tests/test_return_datatype_suffixes.m @@ -0,0 +1,8 @@ +function test_return_datatype_suffixes + + schema = bids.internal.load_schema(); + + suffixes = bids.internal.return_datatype_suffixes(schema.datatypes.func(1)); + assert(isequal(suffixes, '\\_(bold|cbv|sbref)')); + +end From aacc8044155882ae50bf910b3b74423643098a20 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 16 Jan 2021 15:06:24 +0100 Subject: [PATCH 06/54] add function to return regular expression for a certain data type --- +bids/+internal/return_datatype_extensions.m | 6 +++--- .../return_datatype_regular_expression.m | 8 ++++++++ +bids/+internal/return_datatype_suffixes.m | 6 +++--- tests/test_return_datatype_extensions.m | 2 +- .../test_return_datatype_regular_expression.m | 19 +++++++++++++++++++ tests/test_return_datatype_suffixes.m | 2 +- 6 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 +bids/+internal/return_datatype_regular_expression.m create mode 100644 tests/test_return_datatype_regular_expression.m diff --git a/+bids/+internal/return_datatype_extensions.m b/+bids/+internal/return_datatype_extensions.m index 55a51bcd..4a59932a 100644 --- a/+bids/+internal/return_datatype_extensions.m +++ b/+bids/+internal/return_datatype_extensions.m @@ -1,6 +1,6 @@ function extensions = return_datatype_extensions(datatype) - extensions = '\\('; + extensions = '('; for iExt = 1:numel(datatype.extensions) if ~strcmp(datatype.extensions{iExt}, '.json') @@ -8,7 +8,7 @@ end end - % Replace final "|" by a ")" - extensions(end) = ')'; + % Replace final "|" by a "){1}" + extensions(end:end + 3) = '){1}'; end diff --git a/+bids/+internal/return_datatype_regular_expression.m b/+bids/+internal/return_datatype_regular_expression.m new file mode 100644 index 00000000..0bbaebbf --- /dev/null +++ b/+bids/+internal/return_datatype_regular_expression.m @@ -0,0 +1,8 @@ +function regular_expression = return_datatype_regular_expression(datatype) + + suffixes = bids.internal.return_datatype_suffixes(datatype); + extensions = bids.internal.return_datatype_extensions(datatype); + + regular_expression = ['^%s.*' suffixes extensions '$']; + +end diff --git a/+bids/+internal/return_datatype_suffixes.m b/+bids/+internal/return_datatype_suffixes.m index 0f64954e..4ce7476a 100644 --- a/+bids/+internal/return_datatype_suffixes.m +++ b/+bids/+internal/return_datatype_suffixes.m @@ -1,12 +1,12 @@ function suffixes = return_datatype_suffixes(datatype) - suffixes = '\\_('; + suffixes = '_('; for iExt = 1:numel(datatype.suffixes) suffixes = [suffixes, datatype.suffixes{iExt}, '|']; %#ok end - % Replace final "|" by a ")" - suffixes(end) = ')'; + % Replace final "|" by a "){1}" + suffixes(end:end + 3) = '){1}'; end diff --git a/tests/test_return_datatype_extensions.m b/tests/test_return_datatype_extensions.m index 4b0be48b..dd9ec4df 100644 --- a/tests/test_return_datatype_extensions.m +++ b/tests/test_return_datatype_extensions.m @@ -3,6 +3,6 @@ schema = bids.internal.load_schema(); extensions = bids.internal.return_datatype_extensions(schema.datatypes.func(1)); - assert(isequal(extensions, '\\(.nii.gz|.nii)')); + assert(isequal(extensions, '(.nii.gz|.nii){1}')); end diff --git a/tests/test_return_datatype_regular_expression.m b/tests/test_return_datatype_regular_expression.m new file mode 100644 index 00000000..7d3fd3db --- /dev/null +++ b/tests/test_return_datatype_regular_expression.m @@ -0,0 +1,19 @@ +function test_return_datatype_regular_expression + + schema = bids.internal.load_schema(); + + regular_expression = bids.internal.return_datatype_regular_expression(schema.datatypes.anat(1)); + + expected_expression = ['^%s.*', ... + '_(T1w|T2w|PDw|T2starw|FLAIR|inplaneT1|inplaneT2|PDT2|angio){1}', ... + '(.nii.gz|.nii){1}$']; + + assert(isequal(regular_expression, expected_expression)); + + data_dir = fullfile(fileparts(mfilename('fullpath')), 'data', 'MoAEpilot', 'sub-01', 'anat'); + subject_name = 'sub-01'; + file = bids.internal.file_utils('List', data_dir, sprintf(expected_expression, subject_name)); + + assert(isequal(file, 'sub-01_T1w.nii.gz')); + +end diff --git a/tests/test_return_datatype_suffixes.m b/tests/test_return_datatype_suffixes.m index 36236ef1..749008db 100644 --- a/tests/test_return_datatype_suffixes.m +++ b/tests/test_return_datatype_suffixes.m @@ -3,6 +3,6 @@ schema = bids.internal.load_schema(); suffixes = bids.internal.return_datatype_suffixes(schema.datatypes.func(1)); - assert(isequal(suffixes, '\\_(bold|cbv|sbref)')); + assert(isequal(suffixes, '_(bold|cbv|sbref){1}')); end From e14da01f60cd175d58afe404a1a5ec07d7409334 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 4 Feb 2021 06:02:54 +0100 Subject: [PATCH 07/54] update return_datatype_entities to new schema structure --- +bids/+internal/return_datatype_entities.m | 8 +++++++- tests/test_return_datatype_entities.m | 12 +----------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/+bids/+internal/return_datatype_entities.m b/+bids/+internal/return_datatype_entities.m index 3c0b7882..8521d5cf 100644 --- a/+bids/+internal/return_datatype_entities.m +++ b/+bids/+internal/return_datatype_entities.m @@ -1,5 +1,11 @@ function entities = return_datatype_entities(datatype) - entities = fieldnames(datatype.entities); + schema = bids.internal.load_schema(); + + entity_names = fieldnames(datatype.entities); + + for i = 1:size(entity_names, 1) + entities{1, i} = schema.entities.(entity_names{i}).entity; %#ok<*AGROW> + end end diff --git a/tests/test_return_datatype_entities.m b/tests/test_return_datatype_entities.m index e9e588d3..d290dc2f 100644 --- a/tests/test_return_datatype_entities.m +++ b/tests/test_return_datatype_entities.m @@ -4,17 +4,7 @@ entities = bids.internal.return_datatype_entities(schema.datatypes.func(1)); - expected_output = { - 'sub' - 'ses' - 'task' - 'acq' - 'ce' - 'rec' - 'dir' - 'run' - 'echo' - 'part'}; + expected_output = {'sub','ses','task','acq','ce','rec','dir','run','echo','part'}; assert(isequal(entities, expected_output)); From c76bf485d6f224de7be27b3e8dabaf57e8f479b5 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 4 Feb 2021 09:57:34 +0100 Subject: [PATCH 08/54] move schema functions --- +bids/{+internal => +schema}/load_schema.m | 0 +bids/{+internal => +schema}/return_datatype_entities.m | 2 +- +bids/layout.m | 2 +- tests/test_load_schema.m | 2 +- tests/test_return_datatype_entities.m | 4 ++-- 5 files changed, 5 insertions(+), 5 deletions(-) rename +bids/{+internal => +schema}/load_schema.m (100%) rename +bids/{+internal => +schema}/return_datatype_entities.m (85%) diff --git a/+bids/+internal/load_schema.m b/+bids/+schema/load_schema.m similarity index 100% rename from +bids/+internal/load_schema.m rename to +bids/+schema/load_schema.m diff --git a/+bids/+internal/return_datatype_entities.m b/+bids/+schema/return_datatype_entities.m similarity index 85% rename from +bids/+internal/return_datatype_entities.m rename to +bids/+schema/return_datatype_entities.m index 8521d5cf..fb15159e 100644 --- a/+bids/+internal/return_datatype_entities.m +++ b/+bids/+schema/return_datatype_entities.m @@ -1,6 +1,6 @@ function entities = return_datatype_entities(datatype) - schema = bids.internal.load_schema(); + schema = bids.schema.load_schema(); entity_names = fieldnames(datatype.entities); diff --git a/+bids/layout.m b/+bids/layout.m index 66318485..95d2fe4d 100644 --- a/+bids/layout.m +++ b/+bids/layout.m @@ -191,7 +191,7 @@ function tolerant_message(tolerant, msg) subject.pet = struct([]); % PET imaging data % use BIDS schema to organizing parsing of subject data - schema = bids.internal.load_schema(); + schema = bids.schema.load_schema(); modalities = fieldnames(schema.modalities); for iModality = 1:numel(modalities) diff --git a/tests/test_load_schema.m b/tests/test_load_schema.m index 18355b2e..0a94f39e 100644 --- a/tests/test_load_schema.m +++ b/tests/test_load_schema.m @@ -2,7 +2,7 @@ function test_load_schema() SCHEMA_DIR = fullfile(fileparts(mfilename('fullpath')), 'schema'); - schema = bids.internal.load_schema(SCHEMA_DIR); + schema = bids.schema.load_schema(SCHEMA_DIR); assert(isfield(schema, 'base')); assert(isfield(schema, 'subfolder_1')); diff --git a/tests/test_return_datatype_entities.m b/tests/test_return_datatype_entities.m index d290dc2f..b439a12a 100644 --- a/tests/test_return_datatype_entities.m +++ b/tests/test_return_datatype_entities.m @@ -1,8 +1,8 @@ function test_return_datatype_entities - schema = bids.internal.load_schema(); + schema = bids.schema.load_schema(); - entities = bids.internal.return_datatype_entities(schema.datatypes.func(1)); + entities = bids.schema.return_datatype_entities(schema.datatypes.func(1)); expected_output = {'sub','ses','task','acq','ce','rec','dir','run','echo','part'}; From 84393d68e73ea0d7646c8d1a83282ecdfc9211c4 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 4 Feb 2021 10:08:01 +0100 Subject: [PATCH 09/54] update tests to moxunit format --- tests/test_file_utils.m | 10 ++++++++- ...ignatures.m => test_function_signatures.m} | 21 ++++++++++++------- tests/test_load_schema.m | 10 ++++++++- tests/test_return_datatype_entities.m | 11 +++++++++- tests/test_return_datatype_extensions.m | 12 +++++++++-- .../test_return_datatype_regular_expression.m | 12 +++++++++-- tests/test_return_datatype_suffixes.m | 12 +++++++++-- 7 files changed, 72 insertions(+), 16 deletions(-) rename tests/{test_functionSignatures.m => test_function_signatures.m} (61%) diff --git a/tests/test_file_utils.m b/tests/test_file_utils.m index 6279b2fb..6c3f4c36 100644 --- a/tests/test_file_utils.m +++ b/tests/test_file_utils.m @@ -1,4 +1,12 @@ -function test_file_utils() +function test_suite = test_file_utils %#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_file_utils_basic() %% test to get certain part of a filename % {'path', 'basename', 'ext', 'filename', 'cpath', 'fpath'} diff --git a/tests/test_functionSignatures.m b/tests/test_function_signatures.m similarity index 61% rename from tests/test_functionSignatures.m rename to tests/test_function_signatures.m index 59730c28..5d78cf7b 100644 --- a/tests/test_functionSignatures.m +++ b/tests/test_function_signatures.m @@ -1,4 +1,12 @@ -function test_functionSignatures(pth) +function test_suite = test_function_signatures %#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_function_signatures_basic() % Test functionSignatures.json file % The functionSignatures file is used by Matlab to provide code suggestions % and completions for functions. @@ -13,12 +21,11 @@ function test_functionSignatures(pth) % % Copyright (C) 2020--, BIDS-MATLAB developers - if ~nargin - % parent directory of the tests directory - pth = fullfile(pwd, '..'); - end - + root_dir = fullfile(fileparts(mfilename('fullpath')), '..'); + % Run a smoke test - see if the file is a readable json signatures = bids.util.jsondecode( ... - fullfile(pth, 'functionSignatures.json')); + fullfile(root_dir, 'functionSignatures.json')); assert(isstruct(signatures)); + +end diff --git a/tests/test_load_schema.m b/tests/test_load_schema.m index 0a94f39e..c0aa145f 100644 --- a/tests/test_load_schema.m +++ b/tests/test_load_schema.m @@ -1,4 +1,12 @@ -function test_load_schema() +function test_suite = test_load_schema %#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_load_schema_basic() SCHEMA_DIR = fullfile(fileparts(mfilename('fullpath')), 'schema'); diff --git a/tests/test_return_datatype_entities.m b/tests/test_return_datatype_entities.m index b439a12a..de7bd38d 100644 --- a/tests/test_return_datatype_entities.m +++ b/tests/test_return_datatype_entities.m @@ -1,4 +1,13 @@ -function test_return_datatype_entities +function test_suite = test_return_datatype_entities %#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_datatype_entities_basic schema = bids.schema.load_schema(); diff --git a/tests/test_return_datatype_extensions.m b/tests/test_return_datatype_extensions.m index dd9ec4df..c73515ac 100644 --- a/tests/test_return_datatype_extensions.m +++ b/tests/test_return_datatype_extensions.m @@ -1,6 +1,14 @@ -function test_return_datatype_extensions +function test_suite = test_return_datatype_extensions %#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_datatype_extensions_basic - schema = bids.internal.load_schema(); + schema = bids.schema.load_schema(); extensions = bids.internal.return_datatype_extensions(schema.datatypes.func(1)); assert(isequal(extensions, '(.nii.gz|.nii){1}')); diff --git a/tests/test_return_datatype_regular_expression.m b/tests/test_return_datatype_regular_expression.m index 7d3fd3db..05d367c5 100644 --- a/tests/test_return_datatype_regular_expression.m +++ b/tests/test_return_datatype_regular_expression.m @@ -1,6 +1,14 @@ -function test_return_datatype_regular_expression +function test_suite = test_return_datatype_regular_expression %#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_datatype_regular_expression_basic - schema = bids.internal.load_schema(); + schema = bids.schema.load_schema(); regular_expression = bids.internal.return_datatype_regular_expression(schema.datatypes.anat(1)); diff --git a/tests/test_return_datatype_suffixes.m b/tests/test_return_datatype_suffixes.m index 749008db..4ff770a9 100644 --- a/tests/test_return_datatype_suffixes.m +++ b/tests/test_return_datatype_suffixes.m @@ -1,6 +1,14 @@ -function test_return_datatype_suffixes +function test_suite = test_return_datatype_suffixes %#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_datatype_suffixes_basic - schema = bids.internal.load_schema(); + schema = bids.schema.load_schema(); suffixes = bids.internal.return_datatype_suffixes(schema.datatypes.func(1)); assert(isequal(suffixes, '_(bold|cbv|sbref){1}')); From 8dac58498a8e2c0c15a30e6de8c6813920702452 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 4 Feb 2021 10:31:43 +0100 Subject: [PATCH 10/54] add test for parse_filename --- +bids/+internal/parse_filename.m | 3 +- tests/test_parse_filename.m | 74 ++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 tests/test_parse_filename.m diff --git a/+bids/+internal/parse_filename.m b/+bids/+internal/parse_filename.m index 681a2c8e..99a3742f 100644 --- a/+bids/+internal/parse_filename.m +++ b/+bids/+internal/parse_filename.m @@ -50,7 +50,8 @@ try p = orderfields(p, ['filename', 'ext', 'type', fields]); catch - warning('Ignoring file ''%s'' not matching template.', filename); + warning('bidsMatlab:noMatchingTemplate', ... + 'Ignoring file %s not matching template.', filename); p = struct([]); end end diff --git a/tests/test_parse_filename.m b/tests/test_parse_filename.m new file mode 100644 index 00000000..55cd354e --- /dev/null +++ b/tests/test_parse_filename.m @@ -0,0 +1,74 @@ +function test_suite = test_parse_filename %#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_parse_filename_basic() + + filename = '../sub-16/anat/sub-16_ses-mri_run-1_echo-2_FLASH.nii.gz'; + output = bids.internal.parse_filename(filename); + + expected = struct( ... + 'filename', 'sub-16_ses-mri_run-1_echo-2_FLASH.nii.gz', ... + 'type', 'FLASH', ... + 'ext', '.nii.gz', ... + 'sub', '16', ... + 'ses', 'mri', ... + 'run', '1', ... + 'echo', '2'); + + assertEqual(output, expected); + +end + +function test_parse_filename_fields() + + filename = '../sub-16/anat/sub-16_ses-mri_run-1_echo-2_FLASH.nii.gz'; + fields = {'sub', 'ses', 'run', 'echo'}; + output = bids.internal.parse_filename(filename); + + expected = struct( ... + 'filename', 'sub-16_ses-mri_run-1_echo-2_FLASH.nii.gz', ... + 'type', 'FLASH', ... + 'ext', '.nii.gz', ... + 'sub', '16', ... + 'ses', 'mri', ... + 'run', '1', ... + 'echo', '2'); + + assertEqual(output, expected); + +end + +function test_parse_filename_missing_field() + + filename = '../sub-16/anat/sub-16_run-1_echo-2_FLASH.nii.gz'; + fields = {'sub', 'ses', 'run', 'echo'}; + output = bids.internal.parse_filename(filename); + + expected = struct( ... + 'filename', 'sub-16_run-1_echo-2_FLASH.nii.gz', ... + 'type', 'FLASH', ... + 'ext', '.nii.gz', ... + 'sub', '16', ... + 'ses', '', ... + 'run', '1', ... + 'echo', '2'); + + % Would fail as "missing fields" are currently not returned. + % assertEqual(output, expected); + +end + +function test_parse_filename_wrong_template() + + filename = '../sub-16/anat/sub-16_ses-mri_run-1_echo-2_FLASH.nii.gz'; + + assertWarning( ... + @()bids.internal.parse_filename(filename, {'echo'}), ... + 'bidsMatlab:noMatchingTemplate'); + +end From 951077cbd6ff8e63abc539d41a42520aa4aef4e5 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 4 Feb 2021 13:28:08 +0100 Subject: [PATCH 11/54] use schema to parse anat --- +bids/+internal/append_to_structure.m | 62 +++++++++++++++++++++++ +bids/layout.m | 11 ++-- tests/test_append_to_structure.m | 72 +++++++++++++++++++++++++++ tests/test_layout.m | 14 ++++++ 4 files changed, 154 insertions(+), 5 deletions(-) create mode 100644 +bids/+internal/append_to_structure.m create mode 100644 tests/test_append_to_structure.m create mode 100644 tests/test_layout.m diff --git a/+bids/+internal/append_to_structure.m b/+bids/+internal/append_to_structure.m new file mode 100644 index 00000000..3d825d88 --- /dev/null +++ b/+bids/+internal/append_to_structure.m @@ -0,0 +1,62 @@ +function subject = append_to_structure(file, subject, modality) + % Copyright (C) 2021--, BIDS-MATLAB developers + + p = bids.internal.parse_filename(file); + idx = find_suffix_group(modality, p.type); + if isempty(idx) + warning('append_to_structure:noMatchingSuffix', ... + 'Skipping file with no valid suffix in schema: %s', file); + return + end + + schema = bids.schema.load_schema(); + entities = bids.schema.return_datatype_entities(schema.datatypes.(modality)(idx)); + p = bids.internal.parse_filename(file, entities); + + if ~isempty(subject.(modality)) + + missing_fields = setxor(fieldnames(subject.(modality)), fieldnames(p)); + + if ~isempty(missing_fields) + for iField = 1:numel(missing_fields) + p = add_missing_field(p, ... + missing_fields{iField}); + subject.(modality) = add_missing_field(subject.(modality), ... + missing_fields{iField}); + end + end + + end + + subject.(modality) = [subject.(modality) p]; + +end + +function structure = add_missing_field(structure, field) + if ~isfield(structure, field) + structure.(field) = ''; + end +end + +function idx = find_suffix_group(modality, suffix) + + idx = []; + + schema = bids.schema.load_schema(); + suffix_groups = {schema.datatypes.(modality).suffixes}'; + + % 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:numel(suffix_groups) + if any(strcmp(suffix, suffix_groups{i})) + 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/layout.m b/+bids/layout.m index 95d2fe4d..c5fb7b07 100644 --- a/+bids/layout.m +++ b/+bids/layout.m @@ -229,22 +229,23 @@ function tolerant_message(tolerant, msg) function subject = parse_anat(subject) + datatype = 'anat'; + % -------------------------------------------------------------------------- % -Anatomy imaging data % -------------------------------------------------------------------------- - pth = fullfile(subject.path, 'anat'); + pth = fullfile(subject.path, datatype); if exist(pth, 'dir') - entities = return_entities('anat'); - - file_list = return_file_list('anat', subject); + file_list = return_file_list(datatype, subject); for i = 1:numel(file_list) - subject = append_to_structure(file_list{i}, entities, subject, 'anat'); + subject = bids.internal.append_to_structure(file_list{i}, subject, datatype); end + end end diff --git a/tests/test_append_to_structure.m b/tests/test_append_to_structure.m new file mode 100644 index 00000000..161727fd --- /dev/null +++ b/tests/test_append_to_structure.m @@ -0,0 +1,72 @@ +function test_suite = test_append_to_structure %#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_append_to_structure_basic() + + subject = struct('anat', struct([])); + + modality = 'anat'; + + file = '../sub-16/anat/sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz'; + entities = {'sub', 'ses', 'run', 'acq', 'ce', 'rec', 'part'}; + subject = bids.internal.append_to_structure(file, subject, modality); + + expected.anat = struct( ... + 'filename', 'sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz', ... + 'type', 'T1w', ... + 'ext', '.nii.gz', ... + 'sub', '16', ... + 'ses', 'mri', ... + 'run', '1', ... + 'acq', 'hd', ... + 'ce', '', ... + 'rec', '', ... + 'part', ''); + + assertEqual(subject.anat, expected.anat); + +end + +function test_append_to_structure_basic_test() + + subject = struct('anat', struct([])); + modality = 'anat'; + + file = '../sub-16/anat/sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz'; + entities = {'sub', 'ses', 'run', 'acq', 'ce', 'rec', 'part'}; + subject = bids.internal.append_to_structure(file, subject, modality); + + file = '../sub-16/anat/sub-16_ses-mri_run-1_T1map.nii.gz'; + entities = {'sub', 'ses', 'run', 'acq', 'ce', 'rec'}; + subject = bids.internal.append_to_structure(file, subject, modality); + + expected(1).anat = struct( ... + 'filename', 'sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz', ... + 'type', 'T1w', ... + 'ext', '.nii.gz', ... + 'sub', '16', ... + 'ses', 'mri', ... + 'run', '1', ... + 'acq', 'hd', ... + 'ce', '', ... + 'rec', '', ... + 'part', ''); + + expected(2).anat = struct( ... + 'filename', 'sub-16_ses-mri_run-1_T1map.nii.gz', ... + 'type', 'T1map', ... + 'ext', '.nii.gz', ... + 'sub', '16', ... + 'ses', 'mri', ... + 'run', '1', ... + 'acq', '', ... + 'ce', '', ... + 'rec', '', ... + 'part', ''); %#ok<*STRNU> + +end diff --git a/tests/test_layout.m b/tests/test_layout.m new file mode 100644 index 00000000..b4757550 --- /dev/null +++ b/tests/test_layout.m @@ -0,0 +1,14 @@ +function test_suite = test_layout %#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_layout_smoke_test() + + pth_bids_example = get_test_data_dir(); + BIDS = bids.layout(fullfile(pth_bids_example, '7t_trt')); + +end From fe384168dbd8be2ab1cf4c9ccbe6b44e8d179933 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 4 Feb 2021 13:29:01 +0100 Subject: [PATCH 12/54] linting, cleaning, use valid bids example for parse_filename --- +bids/+internal/parse_filename.m | 8 ++--- tests/test_bids_examples.m | 10 +++--- tests/test_function_signatures.m | 4 +-- tests/test_parse_filename.m | 52 ++++++++++++--------------- tests/test_return_datatype_entities.m | 3 +- 5 files changed, 34 insertions(+), 43 deletions(-) diff --git a/+bids/+internal/parse_filename.m b/+bids/+internal/parse_filename.m index 99a3742f..ea5bf894 100644 --- a/+bids/+internal/parse_filename.m +++ b/+bids/+internal/parse_filename.m @@ -4,20 +4,20 @@ % % Example: % - % >> filename = '../sub-16/anat/sub-16_ses-mri_run-1_echo-2_FLASH.nii.gz'; + % >> filename = '../sub-16/anat/sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz'; % >> bids.internal.parse_filename(filename) % % ans = % % struct with fields: % - % filename: 'sub-16_ses-mri_run-1_echo-2_FLASH.nii.gz' - % type: 'FLASH' + % filename: 'sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz' + % type: 'T1w' % ext: '.nii.gz' % sub: '16' % ses: 'mri' % run: '1' - % echo: '2' + % acq: 'hd' % __________________________________________________________________________ % Copyright (C) 2016-2018, Guillaume Flandin, Wellcome Centre for Human Neuroimaging diff --git a/tests/test_bids_examples.m b/tests/test_bids_examples.m index 1cd30282..8db00981 100644 --- a/tests/test_bids_examples.m +++ b/tests/test_bids_examples.m @@ -30,17 +30,17 @@ function test_bids_examples_basic() % -Try to run bids.layout on each dataset directory and keep track of any % failure with a try/catch - sts = false(1, numel(d)); + status = false(1, numel(d)); msg = cell(1, numel(d)); for i = 1:numel(d) if exist(fullfile(pth_bids_example, d(i).name, '.SKIP_VALIDATION'), 'file') - sts(i) = true; + status(i) = true; fprintf('-'); continue end try BIDS = bids.layout(fullfile(pth_bids_example, d(i).name)); - sts(i) = true; + status(i) = true; fprintf('.'); catch err fprintf('X'); @@ -50,8 +50,8 @@ function test_bids_examples_basic() fprintf('\n'); % lists all the folder for which bids.layout failed - if ~all(sts) - for i = find(~sts) + if ~all(status) + for i = find(~status) fprintf('* %s: %s\n', d(i).name, msg{i}); end error('Parsing of BIDS-compatible datasets failed.'); diff --git a/tests/test_function_signatures.m b/tests/test_function_signatures.m index 5d78cf7b..f63b4ad8 100644 --- a/tests/test_function_signatures.m +++ b/tests/test_function_signatures.m @@ -22,10 +22,10 @@ function test_function_signatures_basic() % Copyright (C) 2020--, BIDS-MATLAB developers root_dir = fullfile(fileparts(mfilename('fullpath')), '..'); - + % Run a smoke test - see if the file is a readable json signatures = bids.util.jsondecode( ... fullfile(root_dir, 'functionSignatures.json')); assert(isstruct(signatures)); - + end diff --git a/tests/test_parse_filename.m b/tests/test_parse_filename.m index 55cd354e..3dc2b324 100644 --- a/tests/test_parse_filename.m +++ b/tests/test_parse_filename.m @@ -8,17 +8,29 @@ function test_parse_filename_basic() - filename = '../sub-16/anat/sub-16_ses-mri_run-1_echo-2_FLASH.nii.gz'; + 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_echo-2_FLASH.nii.gz', ... - 'type', 'FLASH', ... + 'filename', 'sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz', ... + 'type', 'T1w', ... 'ext', '.nii.gz', ... 'sub', '16', ... 'ses', 'mri', ... 'run', '1', ... - 'echo', '2'); + 'acq', 'hd'); + + % expected = struct( ... + % 'filename', 'sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz', ... + % 'type', 'T1w', ... + % 'ext', '.nii.gz', ... + % 'sub', '16', ... + % 'ses', 'mri', ... + % 'run', '1', ... + % 'acq', 'hd', ... + % 'ce', '', ... + % 'rec', '', ... + % 'part', ''); assertEqual(output, expected); @@ -26,46 +38,26 @@ function test_parse_filename_basic() function test_parse_filename_fields() - filename = '../sub-16/anat/sub-16_ses-mri_run-1_echo-2_FLASH.nii.gz'; - fields = {'sub', 'ses', 'run', 'echo'}; + filename = '../sub-16/anat/sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz'; + fields = {'sub', 'ses', 'run', 'acq', 'ce', 'rec', 'part'}; output = bids.internal.parse_filename(filename); expected = struct( ... - 'filename', 'sub-16_ses-mri_run-1_echo-2_FLASH.nii.gz', ... - 'type', 'FLASH', ... + 'filename', 'sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz', ... + 'type', 'T1w', ... 'ext', '.nii.gz', ... 'sub', '16', ... 'ses', 'mri', ... 'run', '1', ... - 'echo', '2'); + 'acq', 'hd'); assertEqual(output, expected); end -function test_parse_filename_missing_field() - - filename = '../sub-16/anat/sub-16_run-1_echo-2_FLASH.nii.gz'; - fields = {'sub', 'ses', 'run', 'echo'}; - output = bids.internal.parse_filename(filename); - - expected = struct( ... - 'filename', 'sub-16_run-1_echo-2_FLASH.nii.gz', ... - 'type', 'FLASH', ... - 'ext', '.nii.gz', ... - 'sub', '16', ... - 'ses', '', ... - 'run', '1', ... - 'echo', '2'); - - % Would fail as "missing fields" are currently not returned. - % assertEqual(output, expected); - -end - function test_parse_filename_wrong_template() - filename = '../sub-16/anat/sub-16_ses-mri_run-1_echo-2_FLASH.nii.gz'; + filename = '../sub-16/anat/sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz'; assertWarning( ... @()bids.internal.parse_filename(filename, {'echo'}), ... diff --git a/tests/test_return_datatype_entities.m b/tests/test_return_datatype_entities.m index de7bd38d..6c02fdb9 100644 --- a/tests/test_return_datatype_entities.m +++ b/tests/test_return_datatype_entities.m @@ -6,14 +6,13 @@ initTestSuite; end - function test_return_datatype_entities_basic schema = bids.schema.load_schema(); entities = bids.schema.return_datatype_entities(schema.datatypes.func(1)); - expected_output = {'sub','ses','task','acq','ce','rec','dir','run','echo','part'}; + expected_output = {'sub', 'ses', 'task', 'acq', 'ce', 'rec', 'dir', 'run', 'echo', 'part'}; assert(isequal(entities, expected_output)); From 0e42c5331fbe6e03171a009e75fa3bb41dec3d14 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 4 Feb 2021 13:55:53 +0100 Subject: [PATCH 13/54] use schema to parse behavioral and refactor --- +bids/layout.m | 38 +++----------------------------------- 1 file changed, 3 insertions(+), 35 deletions(-) diff --git a/+bids/layout.m b/+bids/layout.m index c5fb7b07..b3b4d533 100644 --- a/+bids/layout.m +++ b/+bids/layout.m @@ -200,10 +200,8 @@ function tolerant_message(tolerant, msg) for iDatatype = 1:numel(datatypes) switch datatypes{iDatatype} - case 'anat' - subject = parse_anat(subject); - case 'beh' - subject = parse_beh(subject); + case {'anat', 'beh'} + subject = parse_using_schema(subject, datatypes{iDatatype}); case 'dwi' subject = parse_dwi(subject); case 'eeg' @@ -227,9 +225,7 @@ function tolerant_message(tolerant, msg) end -function subject = parse_anat(subject) - - datatype = 'anat'; +function subject = parse_using_schema(subject, datatype) % -------------------------------------------------------------------------- % -Anatomy imaging data @@ -846,28 +842,6 @@ function tolerant_message(tolerant, msg) end -function subject = parse_beh(subject) - % -------------------------------------------------------------------------- - % -Behavioral experiments data - % - % - Event timing, metadata, physiological and other continuous recordings - % -------------------------------------------------------------------------- - pth = fullfile(subject.path, 'beh'); - - if exist(pth, 'dir') - - entities = return_entities('beh'); - - file_list = return_file_list('beh', subject); - - for i = 1:numel(file_list) - - subject = append_to_structure(file_list{i}, entities, subject, 'beh'); - - end - end -end - function subject = parse_dwi(subject) % -------------------------------------------------------------------------- % -Diffusion imaging data @@ -994,9 +968,6 @@ function tolerant_message(tolerant, msg) switch modality - case 'anat' - entities = {'sub', 'ses', 'acq', 'ce', 'rec', 'fa', 'echo', 'inv', 'run'}; - case 'func' entities = {'sub', ... 'ses', ... @@ -1017,9 +988,6 @@ function tolerant_message(tolerant, msg) case 'meg' entities = {'sub', 'ses', 'task', 'acq', 'run', 'proc', 'meta'}; - case 'beh' - entities = {'sub', 'ses', 'task'}; - case 'dwi' entities = {'sub', 'ses', 'acq', 'run', 'bval', 'bvec'}; From 6748545c9d52de01c6886e05d00c08e167fae516 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 4 Feb 2021 14:33:31 +0100 Subject: [PATCH 14/54] parse ASL with schema --- +bids/layout.m | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/+bids/layout.m b/+bids/layout.m index b3b4d533..bbd14517 100644 --- a/+bids/layout.m +++ b/+bids/layout.m @@ -188,6 +188,7 @@ function tolerant_message(tolerant, msg) subject.eeg = struct([]); % EEG data subject.meg = struct([]); % MEG data subject.ieeg = struct([]); % iEEG data + subject.perf = struct([]); % ASL imaging data subject.pet = struct([]); % PET imaging data % use BIDS schema to organizing parsing of subject data @@ -200,7 +201,7 @@ function tolerant_message(tolerant, msg) for iDatatype = 1:numel(datatypes) switch datatypes{iDatatype} - case {'anat', 'beh'} + case {'anat', 'beh', 'perf'} subject = parse_using_schema(subject, datatypes{iDatatype}); case 'dwi' subject = parse_dwi(subject); @@ -214,7 +215,6 @@ function tolerant_message(tolerant, msg) subject = parse_ieeg(subject); case 'meg' subject = parse_meg(subject); - case 'perf' end end @@ -1041,7 +1041,7 @@ function tolerant_message(tolerant, msg) switch modality - case 'anat' + case {'anat', 'dwi', 'perf'} pattern = '_([a-zA-Z0-9]+){1}\\.nii(\\.gz)?'; case 'func' @@ -1059,9 +1059,6 @@ function tolerant_message(tolerant, msg) case 'beh' pattern = '_(events\\.tsv|beh\\.json|physio\\.tsv\\.gz|stim\\.tsv\\.gz)'; - case 'dwi' - pattern = '_([a-zA-Z0-9]+){1}\\.nii(\\.gz)?'; - case 'pet' pattern = '_task-.*_pet\\.nii(\\.gz)?'; From 3795b6b29445b7d7d02afc5d76ce3eea4af37053 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 4 Feb 2021 14:45:46 +0100 Subject: [PATCH 15/54] parse dwi using schema --- +bids/layout.m | 74 ++++++++++++++++++++++++-------------------------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/+bids/layout.m b/+bids/layout.m index bbd14517..9949cfab 100644 --- a/+bids/layout.m +++ b/+bids/layout.m @@ -246,6 +246,41 @@ function tolerant_message(tolerant, msg) end +function subject = parse_dwi(subject) + % -------------------------------------------------------------------------- + % -Diffusion imaging data + % -------------------------------------------------------------------------- + datatype = 'dwi'; + pth = fullfile(subject.path, datatype); + + if exist(pth, 'dir') + + file_list = return_file_list(datatype, subject); + + for i = 1:numel(file_list) + + subject = bids.internal.append_to_structure(file_list{i}, subject, datatype); + + % -bval file + % ------------------------------------------------------------------ + % bval file can also be stored at higher levels (inheritance principle) + bvalfile = bids.internal.get_metadata(file_list{i}, '^.*%s\\.bval$'); + if isfield(bvalfile, 'filename') + subject.dwi(end).bval = bids.util.tsvread(bvalfile.filename); % ? + end + + % -bvec file + % ------------------------------------------------------------------ + % bvec file can also be stored at higher levels (inheritance principle) + bvecfile = bids.internal.get_metadata(file_list{i}, '^.*%s\\.bvec$'); + if isfield(bvalfile, 'filename') + subject.dwi(end).bvec = bids.util.tsvread(bvecfile.filename); % ? + end + + end + end +end + function subject = parse_func(subject) % -------------------------------------------------------------------------- @@ -842,42 +877,6 @@ function tolerant_message(tolerant, msg) end -function subject = parse_dwi(subject) - % -------------------------------------------------------------------------- - % -Diffusion imaging data - % -------------------------------------------------------------------------- - pth = fullfile(subject.path, 'dwi'); - - if exist(pth, 'dir') - - entities = return_entities('dwi'); - - file_list = return_file_list('dwi', subject); - - for i = 1:numel(file_list) - - subject = append_to_structure(file_list{i}, entities, subject, 'dwi'); - - % -bval file - % ------------------------------------------------------------------ - % bval file can also be stored at higher levels (inheritance principle) - bvalfile = bids.internal.get_metadata(file_list{i}, '^.*%s\\.bval$'); - if isfield(bvalfile, 'filename') - subject.dwi(end).bval = bids.util.tsvread(bvalfile.filename); % ? - end - - % -bvec file - % ------------------------------------------------------------------ - % bvec file can also be stored at higher levels (inheritance principle) - bvecfile = bids.internal.get_metadata(file_list{i}, '^.*%s\\.bvec$'); - if isfield(bvalfile, 'filename') - subject.dwi(end).bvec = bids.util.tsvread(bvecfile.filename); % ? - end - - end - end -end - function subject = parse_pet(subject) % -------------------------------------------------------------------------- % -Positron Emission Tomography imaging data @@ -988,9 +987,6 @@ function tolerant_message(tolerant, msg) case 'meg' entities = {'sub', 'ses', 'task', 'acq', 'run', 'proc', 'meta'}; - case 'dwi' - entities = {'sub', 'ses', 'acq', 'run', 'bval', 'bvec'}; - case 'pet' entities = {'sub', 'ses', 'task', 'acq', 'rec', 'run'}; From c830bd9337f414194bb660b82d374ca1659518e2 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 4 Feb 2021 14:55:11 +0100 Subject: [PATCH 16/54] linting --- +bids/+schema/load_schema.m | 3 ++- +bids/layout.m | 3 --- tests/test_layout.m | 1 + 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/+bids/+schema/load_schema.m b/+bids/+schema/load_schema.m index 3b9d56c2..ee2bbe39 100644 --- a/+bids/+schema/load_schema.m +++ b/+bids/+schema/load_schema.m @@ -53,7 +53,8 @@ if ~isempty(json_file_list) field_name = bids.internal.file_utils(directory, 'basename'); structure.(field_name) = struct(); - structure.(field_name) = append_json_content_to_structure(structure.(field_name), json_file_list); + structure.(field_name) = append_json_content_to_structure(structure.(field_name), ... + json_file_list); end structure = inspect_subdir(structure, dirs); diff --git a/+bids/layout.m b/+bids/layout.m index 9949cfab..2bf92837 100644 --- a/+bids/layout.m +++ b/+bids/layout.m @@ -227,9 +227,6 @@ function tolerant_message(tolerant, msg) function subject = parse_using_schema(subject, datatype) - % -------------------------------------------------------------------------- - % -Anatomy imaging data - % -------------------------------------------------------------------------- pth = fullfile(subject.path, datatype); if exist(pth, 'dir') diff --git a/tests/test_layout.m b/tests/test_layout.m index b4757550..619d9a56 100644 --- a/tests/test_layout.m +++ b/tests/test_layout.m @@ -10,5 +10,6 @@ function test_layout_smoke_test() pth_bids_example = get_test_data_dir(); BIDS = bids.layout(fullfile(pth_bids_example, '7t_trt')); + BIDS = bids.layout(fullfile(pth_bids_example, 'asl001')); end From 16072de9753a97089f575f653ce934761157ffc7 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 4 Feb 2021 18:03:51 +0100 Subject: [PATCH 17/54] parse func with schema --- +bids/+internal/append_to_structure.m | 2 +- +bids/layout.m | 83 ++++++--------------------- tests/test_bids_query.m | 4 +- 3 files changed, 23 insertions(+), 66 deletions(-) diff --git a/+bids/+internal/append_to_structure.m b/+bids/+internal/append_to_structure.m index 3d825d88..5640756e 100644 --- a/+bids/+internal/append_to_structure.m +++ b/+bids/+internal/append_to_structure.m @@ -34,7 +34,7 @@ function structure = add_missing_field(structure, field) if ~isfield(structure, field) - structure.(field) = ''; + structure(1).(field) = ''; end end diff --git a/+bids/layout.m b/+bids/layout.m index 2bf92837..74931c3f 100644 --- a/+bids/layout.m +++ b/+bids/layout.m @@ -283,40 +283,30 @@ function tolerant_message(tolerant, msg) % -------------------------------------------------------------------------- % -Task imaging data % -------------------------------------------------------------------------- - pth = fullfile(subject.path, 'func'); + datatype = 'func'; + pth = fullfile(subject.path, datatype); if exist(pth, 'dir') - entities = return_entities('func'); - - file_list = return_file_list('func', subject); + file_list = return_file_list(datatype, subject); for i = 1:numel(file_list) - subject = append_to_structure(file_list{i}, entities, subject, 'func'); + subject = bids.internal.append_to_structure(file_list{i}, subject, datatype); subject.func(end).meta = struct([]); % ? - end - - file_list = return_event_file_list('func', subject); + % TODO: + % + % Events, physiological and other continuous recordings file + % can also be stored at higher levels (inheritance principle). + % - for i = 1:numel(file_list) - - subject = append_to_structure(file_list{i}, entities, subject, 'func'); - - subject.func(end).meta = bids.util.tsvread(fullfile(pth, file_list{i})); % ? + if strcmp(subject.func(end).meta, 'events') + subject.func(end).meta = bids.util.tsvread(fullfile(pth, file_list{i})); % ? + end end - file_list = return_physio_stim_file_list('func', subject); - - for i = 1:numel(file_list) - - subject = append_to_structure(file_list{i}, entities, subject, 'func'); - - subject.func(end).meta = struct([]); % ? - - end end end @@ -964,20 +954,6 @@ function tolerant_message(tolerant, msg) switch modality - case 'func' - entities = {'sub', ... - 'ses', ... - 'task', ... - 'acq', ... - 'rec', ... - 'fa', ... - 'echo', ... - 'dir', ... - 'inv', ... - 'run', ... - 'recording', ... - 'meta'}; - case {'eeg', 'ieeg'} entities = {'sub', 'ses', 'task', 'acq', 'run', 'meta'}; @@ -1034,11 +1010,15 @@ function tolerant_message(tolerant, msg) switch modality + % 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 + case {'anat', 'dwi', 'perf'} pattern = '_([a-zA-Z0-9]+){1}\\.nii(\\.gz)?'; case 'func' - pattern = '_task-.*_bold\\.nii(\\.gz)?'; + pattern = '_task-.*\\.nii(\\.gz)|events\\.tsv|physio\\.tsv\\.gz|stim\\.tsv\\.gz?'; case 'fmap' pattern = '\\.nii(\\.gz)?'; @@ -1050,7 +1030,7 @@ function tolerant_message(tolerant, msg) pattern = '_task-.*_meg\\..*[^json]'; case 'beh' - pattern = '_(events\\.tsv|beh\\.json|physio\\.tsv\\.gz|stim\\.tsv\\.gz)'; + pattern = '_task-.*_(events\\.tsv|beh\\.json|physio\\.tsv\\.gz|stim\\.tsv\\.gz)'; case 'pet' pattern = '_task-.*_pet\\.nii(\\.gz)?'; @@ -1085,7 +1065,7 @@ function tolerant_message(tolerant, msg) switch modality - case {'func', 'eeg', 'meg'} + case {'eeg', 'meg'} pattern = '_task-.*_events\\.tsv'; end @@ -1101,31 +1081,6 @@ function tolerant_message(tolerant, msg) end -function file_list = return_physio_stim_file_list(modality, subject) - % - % Physiological and other continuous recordings file - % - % TODO: stim files can also be stored at higher levels (inheritance principle) - % - - switch modality - - case {'func'} - pattern = '_task-.*_(physio|stim)\\.tsv\\.gz'; - - end - - pth = fullfile(subject.path, modality); - - [file_list, d] = bids.internal.file_utils('List', ... - pth, ... - sprintf(['^%s.*' pattern '$'], ... - subject.name)); - - file_list = convert_to_cell(file_list); - -end - function metafile = return_fmap_metadata_file(subject, fmap_file) pth = fullfile(subject.path, 'fmap'); diff --git a/tests/test_bids_query.m b/tests/test_bids_query.m index 211c04d2..31b4e283 100644 --- a/tests/test_bids_query.m +++ b/tests/test_bids_query.m @@ -43,6 +43,9 @@ function test_bids_query_basic() types = {'T1w', 'bold', 'events', 'inplaneT2'}; assert(isequal(bids.query(BIDS, 'types'), types)); + data = bids.query(BIDS, 'data', 'sub', '01', 'task', 'stopsignalwithpseudowordnaming'); + assertEqual(size(data, 1), 4); + mods = {'anat', 'func'}; assert(isequal(bids.query(BIDS, 'modalities'), mods)); assert(isequal(bids.query(BIDS, 'modalities', 'sub', '01'), mods)); @@ -107,7 +110,6 @@ function test_bids_query_modalities() assert(isequal(bids.query(BIDS, 'modalities', 'sub', '01'), mods)); assert(isequal(bids.query(BIDS, 'modalities', 'sub', '01', 'ses', '1'), mods)); - % % this now fails on octave 4.2.2 but not on Matlab % % bids.query(BIDS, 'modalities', 'sub', '01', 'ses', '2') From c0e1934305f5ad133abd70fd30b5766b0ba375b6 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 4 Feb 2021 20:12:04 +0100 Subject: [PATCH 18/54] fix issue of regexp on Octave add a test for it too fix octave bug in CI add print for CI Same --- +bids/+internal/append_to_structure.m | 5 ++--- +bids/+internal/file_utils.m | 1 - +bids/+internal/return_datatype_suffixes.m | 4 +++- +bids/+schema/load_schema.m | 4 ++-- tests/test_file_utils.m | 5 +++++ 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/+bids/+internal/append_to_structure.m b/+bids/+internal/append_to_structure.m index 5640756e..1101e9e2 100644 --- a/+bids/+internal/append_to_structure.m +++ b/+bids/+internal/append_to_structure.m @@ -43,12 +43,11 @@ idx = []; schema = bids.schema.load_schema(); - suffix_groups = {schema.datatypes.(modality).suffixes}'; % 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:numel(suffix_groups) - if any(strcmp(suffix, suffix_groups{i})) + for i = 1:size(schema.datatypes.(modality), 1) + if any(strcmp(suffix, schema.datatypes.(modality)(i).suffixes)) idx = i; break end diff --git a/+bids/+internal/file_utils.m b/+bids/+internal/file_utils.m index 70b8083b..c565803f 100644 --- a/+bids/+internal/file_utils.m +++ b/+bids/+internal/file_utils.m @@ -282,7 +282,6 @@ files = dirs; else - t = regexp(files, expr); if numel(files) == 1 && ~iscell(t) diff --git a/+bids/+internal/return_datatype_suffixes.m b/+bids/+internal/return_datatype_suffixes.m index 4ce7476a..9bb90fc8 100644 --- a/+bids/+internal/return_datatype_suffixes.m +++ b/+bids/+internal/return_datatype_suffixes.m @@ -1,8 +1,10 @@ function suffixes = return_datatype_suffixes(datatype) suffixes = '_('; + + datatype - for iExt = 1:numel(datatype.suffixes) + for iExt = 1:numel(datatype(:).suffixes) suffixes = [suffixes, datatype.suffixes{iExt}, '|']; %#ok end diff --git a/+bids/+schema/load_schema.m b/+bids/+schema/load_schema.m index ee2bbe39..ea2e8915 100644 --- a/+bids/+schema/load_schema.m +++ b/+bids/+schema/load_schema.m @@ -19,7 +19,7 @@ schema = struct(); - [json_file_list, dirs] = bids.internal.file_utils('FPList', SCHEMA_DIR, '^*.json$'); + [json_file_list, dirs] = bids.internal.file_utils('FPList', SCHEMA_DIR, '^.*.json$'); schema = append_json_content_to_structure(schema, json_file_list); @@ -48,7 +48,7 @@ directory = deblank(subdir_list(iDir, :)); - [json_file_list, dirs] = bids.internal.file_utils('FPList', directory, '^*.json$'); + [json_file_list, dirs] = bids.internal.file_utils('FPList', directory, '^.*.json$'); if ~isempty(json_file_list) field_name = bids.internal.file_utils(directory, 'basename'); diff --git a/tests/test_file_utils.m b/tests/test_file_utils.m index 6c3f4c36..36a7c533 100644 --- a/tests/test_file_utils.m +++ b/tests/test_file_utils.m @@ -67,6 +67,11 @@ function test_file_utils_basic() '^test_file_utils.m$'); assert(isequal(file, 'test_file_utils.m')); + file = bids.internal.file_utils('List', ... + test_directory, ... + '^.*.md$'); + assert(isequal(file, 'README.md')); + directory = bids.internal.file_utils('List', ... test_directory, ... 'dir', ... From e092dad650709ae1d5f87fe3c426f009c546d564 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 6 Feb 2021 18:20:11 +0100 Subject: [PATCH 19/54] prepare for rebase --- +bids/layout.m | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/+bids/layout.m b/+bids/layout.m index 74931c3f..5977e38b 100644 --- a/+bids/layout.m +++ b/+bids/layout.m @@ -188,7 +188,6 @@ function tolerant_message(tolerant, msg) subject.eeg = struct([]); % EEG data subject.meg = struct([]); % MEG data subject.ieeg = struct([]); % iEEG data - subject.perf = struct([]); % ASL imaging data subject.pet = struct([]); % PET imaging data % use BIDS schema to organizing parsing of subject data @@ -201,7 +200,7 @@ function tolerant_message(tolerant, msg) for iDatatype = 1:numel(datatypes) switch datatypes{iDatatype} - case {'anat', 'beh', 'perf'} + case {'anat', 'beh'} subject = parse_using_schema(subject, datatypes{iDatatype}); case 'dwi' subject = parse_dwi(subject); @@ -1014,7 +1013,7 @@ function tolerant_message(tolerant, msg) % 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 - case {'anat', 'dwi', 'perf'} + case {'anat', 'dwi'} pattern = '_([a-zA-Z0-9]+){1}\\.nii(\\.gz)?'; case 'func' From a3b013485939f98ea213898d13fc2b2d79b7ce5b Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 6 Feb 2021 18:59:02 +0100 Subject: [PATCH 20/54] handle asl file parsing with schema --- +bids/layout.m | 64 ++++++++++++++------------------------------------ 1 file changed, 17 insertions(+), 47 deletions(-) diff --git a/+bids/layout.m b/+bids/layout.m index 5977e38b..9c2a99ea 100644 --- a/+bids/layout.m +++ b/+bids/layout.m @@ -146,7 +146,7 @@ fullfile(BIDS.dir, sub{iSub}), ... 'dir', ... '^ses-.*$')); - + for iSess = 1:numel(sess) if isempty(BIDS.subjects) BIDS.subjects = parse_subject(BIDS.dir, sub{iSub}, sess{iSess}); @@ -154,7 +154,7 @@ BIDS.subjects(end + 1) = parse_subject(BIDS.dir, sub{iSub}, sess{iSess}); end end - + end end @@ -214,6 +214,8 @@ function tolerant_message(tolerant, msg) subject = parse_ieeg(subject); case 'meg' subject = parse_meg(subject); + case 'perf' + subject = parse_perf(subject); end end @@ -314,17 +316,18 @@ function tolerant_message(tolerant, msg) % -------------------------------------------------------------------------- % -ASL perfusion imaging data % -------------------------------------------------------------------------- + datatype = 'perf'; pth = fullfile(subject.path, 'perf'); if exist(pth, 'dir') - entities = return_entities('perf'); + file_list = return_file_list(datatype, subject); - file_list = return_file_list('perf', subject); + for i = 1:numel(file_list) - file_list = convert_to_cell(file_list); + subject = bids.internal.append_to_structure(file_list{i}, subject, datatype); - j = 1; + end % ASL timeseries NIfTI file % ---------------------------------------------------------------------- @@ -338,18 +341,9 @@ function tolerant_message(tolerant, msg) for i = 1:numel(idx) - % Parse filename - % --------------------------- - fb = bids.internal.file_utils(bids.internal.file_utils(file_list{idx(i)}, ... - 'basename'), ... - 'basename'); - - p = bids.internal.parse_filename(file_list{idx(i)}, entities); - - subject = append_to_perf(subject, p, j); + j = idx(i); - % add type - subject.perf(j).type = 'asl'; + fb = bids.internal.file_utils(bids.internal.file_utils(file_list{j}, 'basename'), 'basename'); % Manage JSON-sidecar metadata (REQUIRED) % --------------------------- @@ -485,8 +479,8 @@ function tolerant_message(tolerant, msg) subject.perf(j).labeling_image_filename = [Ffile '.jpg']; end - j = j + 1; end % for i = 1:numel(idx) + end % if any(~cellfun(@isempty, labels)) % -M0scan NIfTI file @@ -498,12 +492,10 @@ function tolerant_message(tolerant, msg) if any(~cellfun(@isempty, labels)) idx = find(~cellfun(@isempty, labels)); for i = 1:numel(idx) - % Parse filename - % --------------------------- - fb = bids.internal.file_utils(bids.internal.file_utils(file_list{idx(i)}, 'basename'), 'basename'); - p = bids.internal.parse_filename(file_list{idx(i)}, entities); - subject = append_to_perf(subject, p, j); + j = idx(i); + + fb = bids.internal.file_utils(bids.internal.file_utils(file_list{j}, 'basename'), 'basename'); % Manage JSON-sidecar metadata (REQUIRED) % --------------------------- @@ -555,31 +547,12 @@ function tolerant_message(tolerant, msg) end end end % for i = 1:numel(idx) - j = j + 1; + end % if any(~cellfun(@isempty, labels)) end % if exist(pth, 'dir') end % function subject = parse_perf(subject) -function subject = append_to_perf(subject, p, j) - - if j == 1 - subject.perf = p; - else - fields_p = fieldnames(p); - for iField = 1:length(fields_p) - subject.perf(j).(fields_p{iField}) = p.(fields_p{iField}); - end - end - - % default to run 1 ((!) TODO: but could be that we need to check this - % at the end!) - if isempty(subject.perf(j).run) - subject.perf(j).run = '1'; - end - -end - function subject = parse_fmap(subject) % % TODO: @@ -962,9 +935,6 @@ function tolerant_message(tolerant, msg) case 'pet' entities = {'sub', 'ses', 'task', 'acq', 'rec', 'run'}; - case 'perf' - entities = {'sub', 'ses', 'acq', 'dir', 'rec', 'run'}; - end end @@ -1038,7 +1008,7 @@ function tolerant_message(tolerant, msg) pattern = '_task-.*_ieeg\\..*[^json]'; case 'perf' - pattern = '_(asl|m0scan)\\.nii(\\.gz)?'; + pattern = '_(asl|m0scan)\\.nii(\\.gz)|aslcontext\\.tsv|asllabeling\\.jpg'; end From 1d4115cdbd9eaeafea0404c63b3e705ae2623d09 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 6 Feb 2021 20:46:51 +0100 Subject: [PATCH 21/54] refactor asl manage m0scan, json, context, labelling --- +bids/layout.m | 308 ++++++++++++++++++++++++++++--------------------- 1 file changed, 177 insertions(+), 131 deletions(-) diff --git a/+bids/layout.m b/+bids/layout.m index 9c2a99ea..14c8cce2 100644 --- a/+bids/layout.m +++ b/+bids/layout.m @@ -343,141 +343,16 @@ function tolerant_message(tolerant, msg) j = idx(i); - fb = bids.internal.file_utils(bids.internal.file_utils(file_list{j}, 'basename'), 'basename'); - - % Manage JSON-sidecar metadata (REQUIRED) - % --------------------------- - metafile = fullfile(pth, bids.internal.file_utils(fb, 'ext', 'json')); - - if exist(metafile, 'file') - [~, Ffile] = fileparts(metafile); - subject.perf(j).json_sidecar_filename = [Ffile '.json']; - subject.perf(j).meta = bids.util.jsondecode(metafile); - else - warning(['Missing: ' metafile]); - - end - - % Manage ASLCONTEXT-sidecar metadata (REQUIRED) - % --------------------------- - metafile = fullfile(pth, bids.internal.file_utils([fb(1:end - 4) '_aslcontext'], 'ext', 'tsv')); - - if exist(metafile, 'file') - [~, Ffile] = fileparts(metafile); - subject.perf(j).context_sidecar_filename = [Ffile '.tsv']; - subject.perf(j).context = bids.util.tsvread(metafile); - - else - warning(['Missing: ' metafile]); - - end - - % Manage M0 (REQUIRED) - % --------------------------- - % M0 field is flexible: - - if ~isfield(subject.perf(j).meta, 'M0Type') - warning(['M0Type field missing in ' subject.perf(j).json_sidecar_filename]); - - else - - m0_type = []; - m0_explanation = []; - m0_volume_index = []; - m0_value = []; - - switch subject.perf(j).meta.M0Type - - case 'Separate' - % the M0 was obtained as a separate scan - m0_type = 'separate_scan'; - m0_explanation = 'M0 was obtained as a separate scan'; + subject.perf(j).meta = []; + subject.perf(j).dependencies = []; - % M0scan.nii filename - % assuming the (.nii|.nii.gz) extension choice is the same throughout - m0_filename = [subject.perf(j).filename(1:end - 10) 'm0scan' subject.perf(j).ext]; - if ~exist(fullfile(pth, m0_filename), 'file') - warning(['Missing: ' m0_filename]); - else - % subject.perf(j).m0_filename = m0_filename; - % -> this is included in the same structure for the m0scan.nii - end - - % M0 sidecar filename - m0_json_sidecar_filename = [subject.perf(j).filename(1:end - 10) 'm0scan.json']; - if ~exist(fullfile(pth, m0_json_sidecar_filename), 'file') - warning(['Missing: ' m0_json_sidecar_filename]); - else - % subject.perf(j).m0_json_sidecar_filename = m0_json_sidecar_filename; - % -> this is included in the same structure for the m0scan.nii - end - - case 'Included' - % M0 is one or more image(s) in the *asl.nii[.gz] timeseries - if ~isfield(subject.perf(j), 'context') || ... - ~isfield(subject.perf(j).context, 'volume_type') - warning('Cannot find M0 volume in aslcontext, context-information missing'); - - else - m0indices = find(cellfun(@(x) strcmp(x, 'm0scan'), ... - subject.perf(j).context.volume_type) == true); - if isempty(m0indices) - warning('No M0 volume found in aslcontext'); - - else - m0_type = 'within_timeseries'; - m0_explanation = 'M0 is one or more image(s) in the *asl.nii[.gz] timeseries'; - m0_volume_index = m0indices; - - end - end - - case 'Estimate' - m0_type = 'single_value'; - m0_explanation = [ ... - 'this is a single estimated M0 value, ', ... - 'e.g. when the M0 is obtained from an external scan and/or study']; - m0_value = subject.perf(j).meta.M0; - - case 'Absent' - m0_type = 'use_control_as_m0'; - m0_explanation = [ ... - 'M0 is absent, so we can use the (average) control volume ', ... - 'as pseudo-M0 (if no background suppression was used)']; - - if subject.perf(j).meta.BackgroundSuppression == true - warning('Caution when using control as M0, background suppression was applied'); - end - - otherwise - warning(['Unknown M0Type:', ... - subject.perf(j).meta.M0Type, ... - ' in ', ... - subject.perf(j).json_sidecar_filename]); - end + subject.perf(j) = manage_json_sidecar(subject.perf(j), pth); - if ~isempty(m0_type) - subject.perf(j).m0_type = m0_type; - subject.perf(j).m0_explanation = m0_explanation; - end - - if ~isempty(m0_volume_index) - subject.perf(j).m0_volume_index = m0_volume_index; - end - if ~isempty(m0_value) - subject.perf(j).m0_value = m0_value; - end + subject.perf(j) = manage_aslcontext(subject.perf(j), pth); - end + subject.perf(j) = manage_asllabeling(subject.perf(j), pth); - % Manage labeling image metadata (OPTIONAL) - % --------------------------- - metafile = fullfile(pth, bids.internal.file_utils([fb(1:end - 4) '_labeling'], 'ext', 'jpg')); - - if exist(metafile, 'file') - [~, Ffile] = fileparts(metafile); - subject.perf(j).labeling_image_filename = [Ffile '.jpg']; - end + subject.perf(j) = manage_M0(subject.perf(j), pth); end % for i = 1:numel(idx) @@ -553,6 +428,177 @@ function tolerant_message(tolerant, msg) end % if exist(pth, 'dir') end % function subject = parse_perf(subject) +function structure = manage_json_sidecar(structure, pth) + + % Manage JSON-sidecar metadata (REQUIRED) + % --------------------------- + metafile = fullfile(pth, strrep(structure.filename, structure.ext, '.json')); + + if exist(metafile, 'file') + [~, Ffile] = fileparts(metafile); + structure.dependencies.sidecar = [Ffile '.json']; + structure.meta = bids.util.jsondecode(metafile); + else + warning(['Missing: ' metafile]); + + end + +end + +function perf = manage_aslcontext(perf, pth) + + % ASLCONTEXT-sidecar metadata (REQUIRED) + % --------------------------- + metafile = fullfile(pth, strrep(perf.filename, ... + ['_asl' perf.ext], ... + '_aslcontext.tsv')); + + if exist(metafile, 'file') + [~, Ffile] = fileparts(metafile); + perf.dependencies.context.sidecar = [Ffile '.tsv']; + perf.dependencies.context.content = bids.util.tsvread(metafile); + + else + warning(['Missing: ' metafile]); + + end + +end + +function perf = manage_asllabeling(perf, pth) + % labeling image metadata (OPTIONAL) + % --------------------------- + metafile = fullfile(pth, strrep(perf.filename, ... + ['_asl' perf.ext], ... + '_asllabeling.jpg')); + + if exist(metafile, 'file') + [~, Ffile] = fileparts(metafile); + perf.dependencies.labeling_image = [Ffile '.jpg']; + + end + +end + +function perf = manage_M0(perf, pth) + + % M0 field is flexible: + + if ~isfield(perf.meta, 'M0Type') + warning(['M0Type field missing in ' perf.dependencies.sidecar]); + + else + + m0_type = []; + m0_explanation = []; + m0_volume_index = []; + m0_value = []; + m0_filename = []; + m0_sidecar = []; + + switch perf.meta.M0Type + + case 'Separate' + % the M0 was obtained as a separate scan + m0_type = 'separate_scan'; + m0_explanation = 'M0 was obtained as a separate scan'; + + % M0scan.nii filename + % assuming the (.nii|.nii.gz) extension choice is the same throughout + m0_filename = strrep(perf.filename, ... + ['_asl' perf.ext], ... + ['_m0scan' perf.ext]); + + if ~exist(fullfile(pth, m0_filename), 'file') + warning(['Missing: ' m0_filename]); + else + % subject.perf(j).m0_filename = m0_filename; + % -> this is included in the same structure for the m0scan.nii + end + + % M0 sidecar filename + m0_sidecar = strrep(perf.filename, ... + ['_asl' perf.ext], ... + '_m0scan.json'); + + if ~exist(fullfile(pth, m0_sidecar), 'file') + warning(['Missing: ' m0_sidecar]); + + else + % subject.perf(j).m0_json_sidecar_filename = m0_json_sidecar_filename; + % -> this is included in the same structure for the m0scan.nii + end + + case 'Included' + % M0 is one or more image(s) in the *asl.nii[.gz] timeseries + if ~isfield(perf.dependencies, 'context') || ... + ~isfield(perf.dependencies.context.content, 'volume_type') + warning('Cannot find M0 volume in aslcontext, context-information missing'); + + else + m0indices = find(cellfun(@(x) strcmp(x, 'm0scan'), ... + perf.dependencies.context.content.volume_type) == true); + + if isempty(m0indices) + warning('No M0 volume found in aslcontext'); + + else + m0_type = 'within_timeseries'; + m0_explanation = 'M0 is one or more image(s) in the *asl.nii[.gz] timeseries'; + m0_volume_index = m0indices; + + end + end + + case 'Estimate' + m0_type = 'single_value'; + m0_explanation = [ ... + 'this is a single estimated M0 value, ', ... + 'e.g. when the M0 is obtained from an external scan and/or study']; + m0_value = perf.meta.M0; + + case 'Absent' + m0_type = 'use_control_as_m0'; + m0_explanation = [ ... + 'M0 is absent, so we can use the (average) control volume ', ... + 'as pseudo-M0 (if no background suppression was used)']; + + if perf.meta.BackgroundSuppression == true + warning('Caution when using control as M0, background suppression was applied'); + end + + otherwise + warning(['Unknown M0Type:', ... + perf.meta.M0Type, ... + ' in ', ... + perf.json_sidecar_filename]); + end + + if ~isempty(m0_type) + perf.dependencies.m0.type = m0_type; + perf.dependencies.m0.explanation = m0_explanation; + end + + if ~isempty(m0_volume_index) + perf.dependencies.m0.volume_index = m0_volume_index; + end + + if ~isempty(m0_value) + perf.dependencies.m0.value = m0_value; + end + + if ~isempty(m0_filename) + perf.dependencies.m0.filename = m0_filename; + end + + if ~isempty(m0_sidecar) + perf.dependencies.m0.sidecar = m0_sidecar; + end + + end + +end + function subject = parse_fmap(subject) % % TODO: From 2656a5f395df709647e831305b0164d1d9e5de13 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 6 Feb 2021 21:06:47 +0100 Subject: [PATCH 22/54] refactor managing intended_for for ASL --- +bids/layout.m | 109 ++++++++++++++++++------------------ tests/test_bids_query_asl.m | 16 ++---- 2 files changed, 62 insertions(+), 63 deletions(-) diff --git a/+bids/layout.m b/+bids/layout.m index 14c8cce2..0e7b048c 100644 --- a/+bids/layout.m +++ b/+bids/layout.m @@ -354,9 +354,9 @@ function tolerant_message(tolerant, msg) subject.perf(j) = manage_M0(subject.perf(j), pth); - end % for i = 1:numel(idx) + end - end % if any(~cellfun(@isempty, labels)) + end % -M0scan NIfTI file % --------------------------------------------------------------------- @@ -370,62 +370,16 @@ function tolerant_message(tolerant, msg) j = idx(i); - fb = bids.internal.file_utils(bids.internal.file_utils(file_list{j}, 'basename'), 'basename'); - - % Manage JSON-sidecar metadata (REQUIRED) - % --------------------------- - metafile = fullfile(pth, bids.internal.file_utils(fb, 'ext', 'json')); - - if ~exist(metafile, 'file') - warning(['Missing: ' metafile]); - - else - [~, Ffile] = fileparts(metafile); - subject.perf(j).json_sidecar_filename = [Ffile '.json']; - subject.perf(j).meta = bids.util.jsondecode(metafile); - - % Manage intended-for (REQUIRED) - % --------------------------- - - % Get all NIfTIs that this m0scan is intended for - path_intended_for = {}; - if ~isfield(subject.perf(j).meta, 'IntendedFor') - warning(['Missing field IntendedFor in ' metafile]); - - elseif ischar(subject.perf(j).meta.IntendedFor) - path_intended_for{1} = subject.perf(j).meta.IntendedFor; - - elseif isstruct(subject.perf(j).meta.IntendedFor) - for iPath = 1:length(subject.perf(j).meta.IntendedFor) - path_intended_for{iPath} = subject.perf(j).meta.IntendedFor(iPath); %#ok<*AGROW> - end - - end - - for iPath = 1:length(path_intended_for) - % check if this NIfTI is not missing - if ~exist(fullfile(fileparts(pth), path_intended_for{iPath}), 'file') - warning(['Missing: ' fullfile(fileparts(pth), path_intended_for{iPath})]); - - else - % also check that this NIfTI aims to the same m0scan - [~, path2check, ext2check] = fileparts(path_intended_for{iPath}); - filename_found = max(arrayfun(@(x) strcmp(x.filename, [path2check ext2check]), subject.perf)); - if ~filename_found - warning(['Did not find NIfTI for which is intended: ' subject.perf(j).filename]); + subject.perf(j).intended_for = []; - else - subject.perf(j).intended_for = path_intended_for{iPath}; + subject.perf(j) = manage_intended_for(subject.perf(j), subject, pth); - end - end - end - end - end % for i = 1:numel(idx) + end - end % if any(~cellfun(@isempty, labels)) + end end % if exist(pth, 'dir') + end % function subject = parse_perf(subject) function structure = manage_json_sidecar(structure, pth) @@ -599,6 +553,55 @@ function tolerant_message(tolerant, msg) end +function structure = manage_intended_for(structure, subject, pth) + + structure = manage_json_sidecar(structure, pth); + + if isempty(structure.meta) + return + + else + + % Get all NIfTIs that this m0scan is intended for + path_intended_for = {}; + if ~isfield(structure.meta, 'IntendedFor') + warning(['Missing field IntendedFor in ' structure.dependencies.sidecar]); + + elseif ischar(structure.meta.IntendedFor) + path_intended_for{1} = structure.meta.IntendedFor; + + elseif isstruct(structure.meta.IntendedFor) + for iPath = 1:length(structure.meta.IntendedFor) + path_intended_for{iPath} = structure.meta.IntendedFor(iPath); %#ok<*AGROW> + end + + end + + for iPath = 1:length(path_intended_for) + % check if this NIfTI is not missing + if ~exist(fullfile(fileparts(pth), path_intended_for{iPath}), 'file') + warning(['Missing: ' fullfile(fileparts(pth), path_intended_for{iPath})]); + + else + % also check that this NIfTI aims to the same m0scan + [~, path2check, ext2check] = fileparts(path_intended_for{iPath}); + filename_found = max(arrayfun(@(x) strcmp(x.filename, ... + [path2check ext2check]), ... + subject.perf)); + if ~filename_found + warning(['Did not find NIfTI for which is intended: ' structure.filename]); + + else + structure.intended_for = path_intended_for{iPath}; + + end + end + end + + end + +end + function subject = parse_fmap(subject) % % TODO: diff --git a/tests/test_bids_query_asl.m b/tests/test_bids_query_asl.m index 4211f0ca..a6951f17 100644 --- a/tests/test_bids_query_asl.m +++ b/tests/test_bids_query_asl.m @@ -14,23 +14,21 @@ function test_bids_query_asl_basic() pth_bids_example = get_test_data_dir(); %% 'asl001' - BIDS = bids.layout(fullfile(pth_bids_example, 'asl001 ')); + BIDS = bids.layout(fullfile(pth_bids_example, 'asl001')); modalities = {'anat', 'perf'}; assertEqual(bids.query(BIDS, 'modalities'), modalities); - types = {'T1w', 'asl'}; - % types = {'T1w', 'asl', 'aslcontext', 'asllabelling'}; + types = {'T1w', 'asl', 'aslcontext', 'asllabeling'}; assertEqual(bids.query(BIDS, 'types'), types); %% 'asl002' - BIDS = bids.layout(fullfile(pth_bids_example, 'asl002 ')); + BIDS = bids.layout(fullfile(pth_bids_example, 'asl002')); modalities = {'anat', 'perf'}; assertEqual(bids.query(BIDS, 'modalities'), modalities); - types = {'T1w', 'asl', 'm0scan'}; - % types = {'T1w', 'asl', 'aslcontext', 'asllabelling', 'm0scan'}; + types = {'T1w', 'asl', 'aslcontext', 'asllabeling', 'm0scan'}; assertEqual(bids.query(BIDS, 'types'), types); assertEqual(bids.internal.file_utils(bids.query(BIDS, 'data', 'type', 'm0scan'), 'basename'), ... {'sub-Sub103_m0scan.nii'}); @@ -41,8 +39,7 @@ function test_bids_query_asl_basic() modalities = {'anat', 'perf'}; assertEqual(bids.query(BIDS, 'modalities'), modalities); - types = {'T1w', 'asl', 'm0scan'}; - % types = {'T1w', 'asl', 'aslcontext', 'asllabelling', 'm0scan'}; + types = {'T1w', 'asl', 'aslcontext', 'asllabeling', 'm0scan'}; assertEqual(bids.query(BIDS, 'types'), types); %% 'asl004' @@ -51,8 +48,7 @@ function test_bids_query_asl_basic() modalities = {'anat', 'fmap', 'perf'}; assertEqual(bids.query(BIDS, 'modalities'), modalities); - types = {'T1w', 'asl', 'm0scan'}; - % types = {'T1w', 'asl', 'aslcontext', 'asllabelling', 'm0scan'}; + types = {'T1w', 'asl', 'aslcontext', 'asllabeling', 'm0scan'}; assertEqual(bids.query(BIDS, 'types'), types); end From b0a4f352ffea593f713bcd1d4668bf955d0db13c Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 6 Feb 2021 21:27:24 +0100 Subject: [PATCH 23/54] pass schema as argument instead of reloading it --- +bids/+internal/append_to_structure.m | 9 +++------ +bids/layout.m | 24 ++++++++++++------------ tests/test_append_to_structure.m | 10 +++++++--- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/+bids/+internal/append_to_structure.m b/+bids/+internal/append_to_structure.m index 1101e9e2..efda3537 100644 --- a/+bids/+internal/append_to_structure.m +++ b/+bids/+internal/append_to_structure.m @@ -1,15 +1,14 @@ -function subject = append_to_structure(file, subject, modality) +function subject = append_to_structure(file, subject, modality, schema) % Copyright (C) 2021--, BIDS-MATLAB developers p = bids.internal.parse_filename(file); - idx = find_suffix_group(modality, p.type); + idx = find_suffix_group(modality, p.type, schema); if isempty(idx) warning('append_to_structure:noMatchingSuffix', ... 'Skipping file with no valid suffix in schema: %s', file); return end - schema = bids.schema.load_schema(); entities = bids.schema.return_datatype_entities(schema.datatypes.(modality)(idx)); p = bids.internal.parse_filename(file, entities); @@ -38,12 +37,10 @@ end end -function idx = find_suffix_group(modality, suffix) +function idx = find_suffix_group(modality, suffix, schema) idx = []; - schema = bids.schema.load_schema(); - % 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) diff --git a/+bids/layout.m b/+bids/layout.m index 0e7b048c..5e8aa2cd 100644 --- a/+bids/layout.m +++ b/+bids/layout.m @@ -201,21 +201,21 @@ function tolerant_message(tolerant, msg) for iDatatype = 1:numel(datatypes) switch datatypes{iDatatype} case {'anat', 'beh'} - subject = parse_using_schema(subject, datatypes{iDatatype}); + subject = parse_using_schema(subject, datatypes{iDatatype}, schema); case 'dwi' - subject = parse_dwi(subject); + subject = parse_dwi(subject, schema); case 'eeg' subject = parse_eeg(subject); case 'fmap' subject = parse_fmap(subject); case 'func' - subject = parse_func(subject); + subject = parse_func(subject, schema); case 'ieeg' subject = parse_ieeg(subject); case 'meg' subject = parse_meg(subject); case 'perf' - subject = parse_perf(subject); + subject = parse_perf(subject, schema); end end @@ -226,7 +226,7 @@ function tolerant_message(tolerant, msg) end -function subject = parse_using_schema(subject, datatype) +function subject = parse_using_schema(subject, datatype, schema) pth = fullfile(subject.path, datatype); @@ -236,7 +236,7 @@ function tolerant_message(tolerant, msg) for i = 1:numel(file_list) - subject = bids.internal.append_to_structure(file_list{i}, subject, datatype); + subject = bids.internal.append_to_structure(file_list{i}, subject, datatype, schema); end @@ -244,7 +244,7 @@ function tolerant_message(tolerant, msg) end -function subject = parse_dwi(subject) +function subject = parse_dwi(subject, schema) % -------------------------------------------------------------------------- % -Diffusion imaging data % -------------------------------------------------------------------------- @@ -257,7 +257,7 @@ function tolerant_message(tolerant, msg) for i = 1:numel(file_list) - subject = bids.internal.append_to_structure(file_list{i}, subject, datatype); + subject = bids.internal.append_to_structure(file_list{i}, subject, datatype, schema); % -bval file % ------------------------------------------------------------------ @@ -279,7 +279,7 @@ function tolerant_message(tolerant, msg) end end -function subject = parse_func(subject) +function subject = parse_func(subject, schema) % -------------------------------------------------------------------------- % -Task imaging data @@ -293,7 +293,7 @@ function tolerant_message(tolerant, msg) for i = 1:numel(file_list) - subject = bids.internal.append_to_structure(file_list{i}, subject, datatype); + subject = bids.internal.append_to_structure(file_list{i}, subject, datatype, schema); subject.func(end).meta = struct([]); % ? % TODO: @@ -311,7 +311,7 @@ function tolerant_message(tolerant, msg) end end -function subject = parse_perf(subject) +function subject = parse_perf(subject, schema) % -------------------------------------------------------------------------- % -ASL perfusion imaging data @@ -325,7 +325,7 @@ function tolerant_message(tolerant, msg) for i = 1:numel(file_list) - subject = bids.internal.append_to_structure(file_list{i}, subject, datatype); + subject = bids.internal.append_to_structure(file_list{i}, subject, datatype, schema); end diff --git a/tests/test_append_to_structure.m b/tests/test_append_to_structure.m index 161727fd..fcc8cf8d 100644 --- a/tests/test_append_to_structure.m +++ b/tests/test_append_to_structure.m @@ -8,13 +8,15 @@ function test_append_to_structure_basic() + schema = bids.schema.load_schema(); + subject = struct('anat', struct([])); modality = 'anat'; file = '../sub-16/anat/sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz'; entities = {'sub', 'ses', 'run', 'acq', 'ce', 'rec', 'part'}; - subject = bids.internal.append_to_structure(file, subject, modality); + subject = bids.internal.append_to_structure(file, subject, modality, schema); expected.anat = struct( ... 'filename', 'sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz', ... @@ -34,16 +36,18 @@ function test_append_to_structure_basic() function test_append_to_structure_basic_test() + schema = bids.schema.load_schema(); + subject = struct('anat', struct([])); modality = 'anat'; file = '../sub-16/anat/sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz'; entities = {'sub', 'ses', 'run', 'acq', 'ce', 'rec', 'part'}; - subject = bids.internal.append_to_structure(file, subject, modality); + subject = bids.internal.append_to_structure(file, subject, modality, schema); file = '../sub-16/anat/sub-16_ses-mri_run-1_T1map.nii.gz'; entities = {'sub', 'ses', 'run', 'acq', 'ce', 'rec'}; - subject = bids.internal.append_to_structure(file, subject, modality); + subject = bids.internal.append_to_structure(file, subject, modality, schema); expected(1).anat = struct( ... 'filename', 'sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz', ... From 60c4cd4dabc705cbca0bc286a29a7e20b6af7cd5 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 6 Feb 2021 23:19:53 +0100 Subject: [PATCH 24/54] refactor handling of loading of metadata in layout --- +bids/+internal/get_metadata.m | 13 +++-- +bids/layout.m | 99 +++++++++++++++------------------- 2 files changed, 52 insertions(+), 60 deletions(-) diff --git a/+bids/+internal/get_metadata.m b/+bids/+internal/get_metadata.m index 612658f1..1c822eda 100644 --- a/+bids/+internal/get_metadata.m +++ b/+bids/+internal/get_metadata.m @@ -73,9 +73,15 @@ end - % ========================================================================== - % -Inheritance principle - % ========================================================================== + if isempty(meta) + warning('No metadata for %s', filename); + end + +end + +% ========================================================================== +% -Inheritance principle +% ========================================================================== function s1 = update_metadata(s1, s2, file) if isempty(s2) return @@ -88,3 +94,4 @@ s1.(fn{i}) = s2.(fn{i}); end end +end diff --git a/+bids/layout.m b/+bids/layout.m index 5e8aa2cd..6b48e7cd 100644 --- a/+bids/layout.m +++ b/+bids/layout.m @@ -346,7 +346,11 @@ function tolerant_message(tolerant, msg) subject.perf(j).meta = []; subject.perf(j).dependencies = []; - subject.perf(j) = manage_json_sidecar(subject.perf(j), pth); + subject.perf(j).meta = bids.internal.get_metadata( ... + fullfile( ... + subject.path, ... + datatype, ... + file_list{j})); subject.perf(j) = manage_aslcontext(subject.perf(j), pth); @@ -372,6 +376,12 @@ function tolerant_message(tolerant, msg) subject.perf(j).intended_for = []; + subject.perf(j).meta = bids.internal.get_metadata( ... + fullfile( ... + subject.path, ... + datatype, ... + file_list{j})); + subject.perf(j) = manage_intended_for(subject.perf(j), subject, pth); end @@ -380,23 +390,6 @@ function tolerant_message(tolerant, msg) end % if exist(pth, 'dir') -end % function subject = parse_perf(subject) - -function structure = manage_json_sidecar(structure, pth) - - % Manage JSON-sidecar metadata (REQUIRED) - % --------------------------- - metafile = fullfile(pth, strrep(structure.filename, structure.ext, '.json')); - - if exist(metafile, 'file') - [~, Ffile] = fileparts(metafile); - structure.dependencies.sidecar = [Ffile '.json']; - structure.meta = bids.util.jsondecode(metafile); - else - warning(['Missing: ' metafile]); - - end - end function perf = manage_aslcontext(perf, pth) @@ -439,7 +432,7 @@ function tolerant_message(tolerant, msg) % M0 field is flexible: if ~isfield(perf.meta, 'M0Type') - warning(['M0Type field missing in ' perf.dependencies.sidecar]); + warning('M0Type field missing for %s', perf.filename); else @@ -555,8 +548,6 @@ function tolerant_message(tolerant, msg) function structure = manage_intended_for(structure, subject, pth) - structure = manage_json_sidecar(structure, pth); - if isempty(structure.meta) return @@ -565,7 +556,7 @@ function tolerant_message(tolerant, msg) % Get all NIfTIs that this m0scan is intended for path_intended_for = {}; if ~isfield(structure.meta, 'IntendedFor') - warning(['Missing field IntendedFor in ' structure.dependencies.sidecar]); + warning('Missing field IntendedFor for %s', structure.filename); elseif ischar(structure.meta.IntendedFor) path_intended_for{1} = structure.meta.IntendedFor; @@ -613,11 +604,13 @@ function tolerant_message(tolerant, msg) % -------------------------------------------------------------------------- % -Fieldmap data % -------------------------------------------------------------------------- - pth = fullfile(subject.path, 'fmap'); + datatype = 'fmap'; + + pth = fullfile(subject.path, datatype); if exist(pth, 'dir') - file_list = return_file_list('fmap', subject); + file_list = return_file_list(datatype, subject); j = 1; @@ -643,12 +636,11 @@ function tolerant_message(tolerant, msg) subject = append_common_fmap_fields_to_structure(subject, labels{idx(i)}, j); - metafile = return_fmap_metadata_file(subject, file_list{idx(i)}); - subject.fmap(j).meta = struct([]); - % (!) TODO: file can also be stored at higher levels (inheritance principle) - if ~isempty(metafile) - subject.fmap(j).meta = bids.util.jsondecode(metafile); - end + subject.fmap(j).meta = bids.internal.get_metadata( ... + fullfile( ... + subject.path, ... + datatype, ... + file_list{j})); j = j + 1; @@ -681,13 +673,13 @@ function tolerant_message(tolerant, msg) subject = append_common_fmap_fields_to_structure(subject, labels{idx(i)}, j); - metafile = return_fmap_metadata_file(subject, file_list{idx(i)}); + json_file = return_jsonfile(subject, file_list{idx(i)}, datatype); subject.fmap(j).meta = struct([]); % (!) TODO: file can also be stored at higher levels (inheritance principle) - if ~isempty(metafile) + if ~isempty(json_file) subject.fmap(j).meta = { ... - bids.util.jsondecode(metafile), ... - bids.util.jsondecode(strrep(metafile, ... + bids.util.jsondecode(json_file), ... + bids.util.jsondecode(strrep(json_file, ... '_phase1.json', ... '_phase2.json'))}; end @@ -716,12 +708,11 @@ function tolerant_message(tolerant, msg) subject = append_common_fmap_fields_to_structure(subject, labels{idx(i)}, j); - metafile = return_fmap_metadata_file(subject, file_list{idx(i)}); - subject.fmap(j).meta = struct([]); - % (!) TODO: file can also be stored at higher levels (inheritance principle) - if ~isempty(metafile) - subject.fmap(j).meta = bids.util.jsondecode(metafile); - end + subject.fmap(j).meta = bids.internal.get_metadata( ... + fullfile( ... + subject.path, ... + datatype, ... + file_list{j})); j = j + 1; @@ -748,12 +739,11 @@ function tolerant_message(tolerant, msg) subject = append_common_fmap_fields_to_structure(subject, labels{idx(i)}, j); - metafile = return_fmap_metadata_file(subject, file_list{idx(i)}); - subject.fmap(j).meta = struct([]); - % (!) TODO: file can also be stored at higher levels (inheritance principle) - if ~isempty(metafile) - subject.fmap(j).meta = bids.util.jsondecode(metafile); - end + subject.fmap(j).meta = bids.internal.get_metadata( ... + fullfile( ... + subject.path, ... + datatype, ... + file_list{j})); j = j + 1; @@ -1019,11 +1009,6 @@ function tolerant_message(tolerant, msg) end -% TODO -% -% more refactoring can be done across the several 'return_X_file_list' functions -% - function file_list = return_file_list(modality, subject) switch modality @@ -1099,18 +1084,18 @@ function tolerant_message(tolerant, msg) end -function metafile = return_fmap_metadata_file(subject, fmap_file) +function json_file = return_jsonfile(subject, filename, modality) - pth = fullfile(subject.path, 'fmap'); + pth = fullfile(subject.path, modality); fb = bids.internal.file_utils(bids.internal.file_utils( ... - fmap_file, ... + filename, ... 'basename'), ... 'basename'); - metafile = fullfile(pth, bids.internal.file_utils(fb, 'ext', 'json')); + json_file = fullfile(pth, bids.internal.file_utils(fb, 'ext', 'json')); - if ~exist(metafile, 'file') - metafile = []; + if ~exist(json_file, 'file') + json_file = []; end end From accf9d3310387ccd8ce60d552f8d89c8c0139796 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 7 Feb 2021 00:14:56 +0100 Subject: [PATCH 25/54] refactor fmap parsing to use schema --- +bids/layout.m | 269 ++++++++++--------------------------------------- 1 file changed, 52 insertions(+), 217 deletions(-) diff --git a/+bids/layout.m b/+bids/layout.m index 6b48e7cd..4e24effc 100644 --- a/+bids/layout.m +++ b/+bids/layout.m @@ -207,7 +207,7 @@ function tolerant_message(tolerant, msg) case 'eeg' subject = parse_eeg(subject); case 'fmap' - subject = parse_fmap(subject); + subject = parse_fmap(subject, schema); case 'func' subject = parse_func(subject, schema); case 'ieeg' @@ -593,162 +593,59 @@ function tolerant_message(tolerant, msg) end -function subject = parse_fmap(subject) - % - % TODO: - % - % 20210114 - From Remi: - % For other modalities, metadata are fetched upon query. - % It is unclear why we do it differently for fmaps +function subject = parse_fmap(subject, schema) - % -------------------------------------------------------------------------- - % -Fieldmap data - % -------------------------------------------------------------------------- datatype = 'fmap'; - pth = fullfile(subject.path, datatype); if exist(pth, 'dir') file_list = return_file_list(datatype, subject); - j = 1; - - % -Phase difference image and at least one magnitude image - % ---------------------------------------------------------------------- - labels = return_labels_fieldmap(file_list, 'phase_difference_image'); - - if any(~cellfun(@isempty, labels)) - - idx = find(~cellfun(@isempty, labels)); - - for i = 1:numel(idx) - - subject.fmap(j).type = 'phasediff'; - subject.fmap(j).filename = file_list{idx(i)}; - subject.fmap(j).magnitude = { ... - strrep(file_list{idx(i)}, ... - '_phasediff.nii', ... - '_magnitude1.nii'), ... - strrep(file_list{idx(i)}, ... - '_phasediff.nii', ... - '_magnitude2.nii')}; % optional - - subject = append_common_fmap_fields_to_structure(subject, labels{idx(i)}, j); - - subject.fmap(j).meta = bids.internal.get_metadata( ... - fullfile( ... - subject.path, ... - datatype, ... - file_list{j})); - - j = j + 1; - - end - end - - % -Two phase images and two magnitude images - % ---------------------------------------------------------------------- - labels = return_labels_fieldmap(file_list, 'two_phase_image'); - - if any(~cellfun(@isempty, labels)) - - idx = find(~cellfun(@isempty, labels)); - - for i = 1:numel(idx) + for i = 1:numel(file_list) - subject.fmap(j).type = 'phase12'; - subject.fmap(j).filename = { ... - file_list{idx(i)}, ... - strrep(file_list{idx(i)}, ... - '_phase1.nii', ... - '_phase2.nii')}; - subject.fmap(j).magnitude = { ... - strrep(file_list{idx(i)}, ... - '_phase1.nii', ... - '_magnitude1.nii'), ... - strrep(file_list{idx(i)}, ... - '_phase1.nii', ... - '_magnitude2.nii')}; - - subject = append_common_fmap_fields_to_structure(subject, labels{idx(i)}, j); - - json_file = return_jsonfile(subject, file_list{idx(i)}, datatype); - subject.fmap(j).meta = struct([]); - % (!) TODO: file can also be stored at higher levels (inheritance principle) - if ~isempty(json_file) - subject.fmap(j).meta = { ... - bids.util.jsondecode(json_file), ... - bids.util.jsondecode(strrep(json_file, ... - '_phase1.json', ... - '_phase2.json'))}; - end + subject = bids.internal.append_to_structure(file_list{i}, subject, datatype, schema); - j = j + 1; + subject.fmap(i).meta = bids.internal.get_metadata( ... + fullfile( ... + subject.path, ... + datatype, ... + file_list{i})); + % subject.perf(i).intended_for = []; + % subject.fmap(i) = manage_intended_for(subject.fmap(i), subject, pth); + + switch subject.fmap(i).type + + % -A single, real fieldmap image + case {'fieldmap', 'magnitude'} + subject.fmap(i).dependencies.magnitude = strrep(file_list{idx(i)}, ... + '_fieldmap.nii', ... + '_magnitude.nii'); + + % -Phase difference image and at least one magnitude image + case {'phasediff'} + subject.fmap(i).dependencies.magnitude = { ... + strrep(file_list{i}, ... + '_phasediff.nii', ... + '_magnitude1.nii'), ... + strrep(file_list{i}, ... + '_phasediff.nii', ... + '_magnitude2.nii')}; % optional + + % -Two phase images and two magnitude images + case {'phase1', 'phase2'} + subject.fmap(i).dependencies.magnitude = { ... + strrep(file_list{i}, ... + '_phase1.nii', ... + '_magnitude1.nii'), ... + strrep(file_list{i}, ... + '_phase1.nii', ... + '_magnitude2.nii')}; end end - % -A single, real fieldmap image - % ---------------------------------------------------------------------- - labels = return_labels_fieldmap(file_list, 'fieldmap_image'); - - if any(~cellfun(@isempty, labels)) - - idx = find(~cellfun(@isempty, labels)); - - for i = 1:numel(idx) - - subject.fmap(j).type = 'fieldmap'; - subject.fmap(j).filename = file_list{idx(i)}; - subject.fmap(j).magnitude = strrep(file_list{idx(i)}, ... - '_fieldmap.nii', ... - '_magnitude.nii'); - - subject = append_common_fmap_fields_to_structure(subject, labels{idx(i)}, j); - - subject.fmap(j).meta = bids.internal.get_metadata( ... - fullfile( ... - subject.path, ... - datatype, ... - file_list{j})); - - j = j + 1; - - end - end - - % -Multiple phase encoded directions (topup) - % ---------------------------------------------------------------------- - labels = return_labels_fieldmap(file_list, 'phase_encoded_direction_image'); - - if any(~cellfun(@isempty, labels)) - - idx = find(~cellfun(@isempty, labels)); - - for i = 1:numel(idx) - - subject.fmap(j).filename = file_list{idx(i)}; - if ~isempty(regexp(subject.fmap(j).filename, 'm0scan', 'ONCE')) - subject.fmap(j).type = 'm0scan'; - else - subject.fmap(j).type = 'epi'; - end - subject.fmap(j).dir = labels{idx(i)}.dir; - - subject = append_common_fmap_fields_to_structure(subject, labels{idx(i)}, j); - - subject.fmap(j).meta = bids.internal.get_metadata( ... - fullfile( ... - subject.path, ... - datatype, ... - file_list{j})); - - j = j + 1; - - end - end end end @@ -945,14 +842,6 @@ function tolerant_message(tolerant, msg) end -function subject = append_common_fmap_fields_to_structure(subject, labels, idx) - - subject.fmap(idx).ses = regexprep(labels.ses, '^_[a-zA-Z0-9]+-', ''); - subject.fmap(idx).acq = regexprep(labels.acq, '^_[a-zA-Z0-9]+-', ''); - subject.fmap(idx).run = regexprep(labels.run, '^_[a-zA-Z0-9]+-', ''); - -end - function f = convert_to_cell(f) if isempty(f) f = {}; @@ -977,38 +866,6 @@ function tolerant_message(tolerant, msg) end end -function labels = return_labels_fieldmap(file_list, fiefmap_type) - - direction_pattern = ''; - - switch fiefmap_type - - case 'phase_difference_image' - suffix = 'phasediff'; - - case 'two_phase_image' - suffix = 'phase1'; - - case 'fieldmap_image' - suffix = 'fieldmap'; - - case 'phase_encoded_direction_image' - suffix = '(epi|m0scan)'; - - direction_pattern = '_dir-(?[a-zA-Z0-9]+)?'; - - end - - labels = regexp(file_list, [ ... - '^sub-[a-zA-Z0-9]+', ... % sub- - '(?_ses-[a-zA-Z0-9]+)?', ... % ses-