Skip to content

Commit 4a60dc6

Browse files
authored
Merge pull request #124 from Remi-Gau/remi-implement_schema
[ENH] implement bids-schema - part 1
2 parents 3c1ac7b + e318a43 commit 4a60dc6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1742
-1257
lines changed

+bids/+internal/add_missing_field.m

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
function structure = add_missing_field(structure, field)
2+
if ~isfield(structure, field)
3+
structure(1).(field) = '';
4+
end
5+
end

+bids/+internal/append_to_layout.m

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
function subject = append_to_layout(file, subject, modality, schema)
2+
%
3+
% appends a file to the BIDS layout by parsing it according to the provided schema
4+
%
5+
% USAGE::
6+
%
7+
% subject = append_to_layout(file, subject, modality, schema == [])
8+
%
9+
% :param file:
10+
% :type file: string
11+
% :param subject: subject sub-structure from the BIDS layout
12+
% :type subject: strcture
13+
% :param modality:
14+
% :type modality: string
15+
% :param schema:
16+
% :type schema: strcture
17+
%
18+
%
19+
% Copyright (C) 2021--, BIDS-MATLAB developers
20+
21+
if ~exist('schema', 'var')
22+
schema = [];
23+
end
24+
25+
% Parse file fist to identify the suffix group in the template.
26+
% Then reparse the file using the entity-label pairs defined in the schema.
27+
p = bids.internal.parse_filename(file);
28+
29+
idx = find_suffix_group(modality, p.suffix, schema);
30+
31+
if ~isempty(schema)
32+
33+
if isempty(idx)
34+
warning('append_to_structure:noMatchingSuffix', ...
35+
'Skipping file with no valid suffix in schema: %s', file);
36+
return
37+
end
38+
39+
entities = bids.schema.return_modality_entities(schema.datatypes.(modality)(idx), schema);
40+
p = bids.internal.parse_filename(file, entities);
41+
42+
end
43+
44+
% Check any new entity field that needs to be added into the layout or the output
45+
% of the parsing to make sure the 2 structures can be concatenated
46+
if ~isempty(subject.(modality))
47+
48+
[subject.(modality), p] = bids.internal.match_structure_fields(subject.(modality), p);
49+
50+
end
51+
52+
if isempty(subject.(modality))
53+
subject.(modality) = p;
54+
else
55+
subject.(modality)(end + 1, 1) = p;
56+
end
57+
58+
end
59+
60+
function idx = find_suffix_group(modality, suffix, schema)
61+
62+
idx = [];
63+
64+
if isempty(schema)
65+
return
66+
end
67+
68+
% the following loop could probably be improved with some cellfun magic
69+
% cellfun(@(x, y) any(strcmp(x,y)), {p.type}, suffix_groups)
70+
for i = 1:size(schema.datatypes.(modality), 1)
71+
72+
this_suffix_group = schema.datatypes.(modality)(i);
73+
74+
% for CI
75+
if iscell(this_suffix_group)
76+
this_suffix_group = this_suffix_group{1};
77+
end
78+
79+
if any(strcmp(suffix, this_suffix_group.suffixes))
80+
idx = i;
81+
break
82+
end
83+
84+
end
85+
86+
if isempty(idx)
87+
warning('findSuffix:noMatchingSuffix', ...
88+
'No corresponding suffix in schema for %s for datatype %s', suffix, modality);
89+
end
90+
91+
end

+bids/+internal/file_utils.m

-1
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,6 @@
282282
files = dirs;
283283

284284
else
285-
286285
t = regexp(files, expr);
287286

288287
if numel(files) == 1 && ~iscell(t)

+bids/+internal/get_metadata.m

+19-8
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
N = 3;
2222

2323
% -There is a session level in the hierarchy
24-
if isfield(p, 'ses') && ~isempty(p.ses)
24+
if isfield(p.entities, 'ses') && ~isempty(p.entities.ses)
2525
N = N + 1;
2626
end
2727

@@ -31,7 +31,7 @@
3131

3232
% -List the potential metadata files associated with this file suffix type
3333
% Default is to assume it is a JSON file
34-
metafile = bids.internal.file_utils('FPList', pth, sprintf(pattern, p.type));
34+
metafile = bids.internal.file_utils('FPList', pth, sprintf(pattern, p.suffix));
3535

3636
if isempty(metafile)
3737
metafile = {};
@@ -44,13 +44,17 @@
4444
for i = 1:numel(metafile)
4545

4646
p2 = bids.internal.parse_filename(metafile{i});
47-
fn = setdiff(fieldnames(p2), {'filename', 'ext', 'type'});
47+
entities = {};
48+
if isfield(p2, 'entities')
49+
entities = fieldnames(p2.entities);
50+
end
4851

4952
% -Check if this metadata file contains the same entity-label pairs as its
5053
% data file counterpart
5154
ismeta = true;
52-
for j = 1:numel(fn)
53-
if ~isfield(p, fn{j}) || ~strcmp(p.(fn{j}), p2.(fn{j}))
55+
for j = 1:numel(entities)
56+
if ~isfield(p.entities, entities{j}) || ...
57+
~strcmp(p.entities.(entities{j}), p2.entities.(entities{j}))
5458
ismeta = false;
5559
break
5660
end
@@ -73,9 +77,15 @@
7377

7478
end
7579

76-
% ==========================================================================
77-
% -Inheritance principle
78-
% ==========================================================================
80+
if isempty(meta)
81+
warning('No metadata for %s', filename);
82+
end
83+
84+
end
85+
86+
% ==========================================================================
87+
% -Inheritance principle
88+
% ==========================================================================
7989
function s1 = update_metadata(s1, s2, file)
8090
if isempty(s2)
8191
return
@@ -88,3 +98,4 @@
8898
s1.(fn{i}) = s2.(fn{i});
8999
end
90100
end
101+
end

+bids/+internal/keep_file_for_query.m

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
function status = keep_file(file_struct, options)
2+
3+
status = true;
4+
5+
% suffix is treated separately as it is not one of the entities
6+
for l = 1:size(options, 1)
7+
if strcmp(options{l, 1}, 'suffix') && ~ismember(file_struct.suffix, options{l, 2})
8+
status = false;
9+
return
10+
end
11+
end
12+
13+
for l = 1:size(options, 1)
14+
15+
if ~strcmp(options{l, 1}, 'suffix')
16+
17+
if ~ismember(options{l, 1}, fieldnames(file_struct.entities))
18+
status = false;
19+
break
20+
end
21+
22+
if isfield(file_struct.entities, options{l, 1}) && ...
23+
~ismember(file_struct.entities.(options{l, 1}), options{l, 2})
24+
status = false;
25+
break
26+
end
27+
28+
end
29+
30+
end
31+
32+
end
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
function [s1, s2] = match_structure_fields(s1, s2)
2+
3+
missing_fields = setxor(fieldnames(s1), fieldnames(s2));
4+
5+
if ~isempty(missing_fields)
6+
for iField = 1:numel(missing_fields)
7+
8+
s1 = bids.internal.add_missing_field(s1, missing_fields{iField});
9+
s2 = bids.internal.add_missing_field(s2, missing_fields{iField});
10+
11+
end
12+
end
13+
14+
end

+bids/+internal/parse_filename.m

+32-20
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,35 @@
11
function p = parse_filename(filename, fields)
2+
%
23
% Split a filename into its building constituents
3-
% FORMAT p = bids.internal.parse_filename(filename, fields)
4+
%
5+
% USAGE::
6+
%
7+
% p = bids.internal.parse_filename(filename, fields)
8+
%
9+
% :param filename: fielname to parse that follows the pattern
10+
% ``sub-label[_entity-label]*_suffix.extension``
11+
% :type filename: string
12+
% :param fields: cell of strings of the entities to use for parsing
13+
% :type fields: cell
414
%
515
% Example:
616
%
7-
% >> filename = '../sub-16/anat/sub-16_ses-mri_run-1_echo-2_FLASH.nii.gz';
8-
% >> bids.internal.parse_filename(filename)
17+
% filename = '../sub-16/anat/sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz';
918
%
10-
% ans =
19+
% bids.internal.parse_filename(filename)
20+
%
21+
% ans =
1122
%
1223
% struct with fields:
1324
%
14-
% filename: 'sub-16_ses-mri_run-1_echo-2_FLASH.nii.gz'
15-
% type: 'FLASH'
16-
% ext: '.nii.gz'
17-
% sub: '16'
18-
% ses: 'mri'
19-
% run: '1'
20-
% echo: '2'
25+
% 'filename', 'sub-16_ses-mri_run-1_acq-hd_T1w.nii.gz', ...
26+
% 'suffix', 'T1w', ...
27+
% 'ext', '.nii.gz', ...
28+
% 'entities', struct('sub', '16', ...
29+
% 'ses', 'mri', ...
30+
% 'run', '1', ...
31+
% 'acq', 'hd');
32+
%
2133
% __________________________________________________________________________
2234

2335
% Copyright (C) 2016-2018, Guillaume Flandin, Wellcome Centre for Human Neuroimaging
@@ -26,31 +38,31 @@
2638
filename = bids.internal.file_utils(filename, 'filename');
2739

2840
% -Identify all the BIDS entity-label pairs present in the filename (delimited by "_")
29-
% https://bids-specification.readthedocs.io/en/stable/99-appendices/04-entity-table.html
3041
[parts, dummy] = regexp(filename, '(?:_)+', 'split', 'match'); %#ok<ASGLU>
3142
p.filename = filename;
3243

3344
% -Identify the suffix and extension of this file
34-
% https://bids-specification.readthedocs.io/en/stable/02-common-principles.html#file-name-structure
35-
[p.type, p.ext] = strtok(parts{end}, '.');
45+
[p.suffix, p.ext] = strtok(parts{end}, '.');
3646

3747
% -Separate the entity from the label for each pair identified above
3848
for i = 1:numel(parts) - 1
3949
[d, dummy] = regexp(parts{i}, '(?:\-)+', 'split', 'match'); %#ok<ASGLU>
40-
p.(d{1}) = d{2};
50+
p.entities.(d{1}) = d{2};
4151
end
4252

4353
% -Extra fields can be added to the structure and ordered specifically.
4454
if nargin == 2
4555
for i = 1:numel(fields)
46-
if ~isfield(p, fields{i})
47-
p.(fields{i}) = '';
48-
end
56+
p.entities = bids.internal.add_missing_field(p.entities, fields{i});
4957
end
5058
try
51-
p = orderfields(p, ['filename', 'ext', 'type', fields]);
59+
p = orderfields(p, {'filename', 'ext', 'suffix', 'entities'});
60+
p.entities = orderfields(p.entities, fields);
5261
catch
53-
warning('Ignoring file ''%s'' not matching template.', filename);
62+
warning('bidsMatlab:noMatchingTemplate', ...
63+
'Ignoring file %s not matching template.', filename);
5464
p = struct([]);
5565
end
5666
end
67+
68+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
function extensions = return_modality_extensions(modality)
2+
3+
extensions = '(';
4+
5+
% for CI
6+
if iscell(modality)
7+
modality = modality{1};
8+
end
9+
10+
for iExt = 1:numel(modality.extensions)
11+
if ~strcmp(modality.extensions{iExt}, '.json')
12+
extensions = [extensions, modality.extensions{iExt}, '|']; %#ok<AGROW>
13+
end
14+
end
15+
16+
% Replace final "|" by a "){1}"
17+
extensions(end:end + 3) = '){1}';
18+
19+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
function regular_expression = return_modality_regular_expression(modality)
2+
3+
suffixes = bids.internal.return_modality_suffixes(modality);
4+
extensions = bids.internal.return_modality_extensions(modality);
5+
6+
regular_expression = ['^%s.*' suffixes extensions '$'];
7+
8+
end
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
function suffixes = return_modality_suffixes(modality)
2+
3+
suffixes = '_(';
4+
5+
% For CI
6+
if iscell(modality)
7+
modality = modality{1};
8+
end
9+
10+
for iExt = 1:numel(modality(:).suffixes)
11+
suffixes = [suffixes, modality.suffixes{iExt}, '|']; %#ok<AGROW>
12+
end
13+
14+
% Replace final "|" by a "){1}"
15+
suffixes(end:end + 3) = '){1}';
16+
17+
end

0 commit comments

Comments
 (0)