Skip to content

Commit 74af953

Browse files
authored
Merge pull request #301 from jasmainak/meg-validator
[MRG] Add validator for MEG
2 parents 593a175 + 7166309 commit 74af953

File tree

12 files changed

+299
-19
lines changed

12 files changed

+299
-19
lines changed

tests/bids.spec.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@ var fs = require('fs');
88
var AdmZip = require('adm-zip');
99
var path = require('path');
1010
var Test = require("mocha/lib/test");
11-
var test_version = "1.0.2u1";
11+
var test_version = "1.0.2u2";
1212

1313
function getDirectories(srcpath) {
1414
return fs.readdirSync(srcpath).filter(function(file) {
1515
return fs.statSync(path.join(srcpath, file)).isDirectory();
1616
});
1717
}
1818

19-
var missing_session_files = ['7t_trt', 'ds006', 'ds007', 'ds008', 'ds051', 'ds052', 'ds105', 'ds108', 'ds109', 'ds113b'];
19+
var missing_session_files = ['7t_trt', 'ds006', 'ds007', 'ds008', 'ds051', 'ds052', 'ds105', 'ds108', 'ds109', 'ds113b',
20+
'ds000117', 'ds000246', 'ds000247'];
2021

2122
function assertErrorCode(errors, expected_error_code) {
2223
var matchingErrors = errors.filter(function (error) {

tests/json.spec.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,29 @@ describe('JSON', function(){
2626
});
2727

2828
it('should detect negative value for SliceTiming', function(){
29-
var jsonObj = '{"SliceTiming": [-1.0, 0.0, 1.0]}';
29+
var jsonObj = '{"RepetitionTime": 1.2, "SliceTiming": [-1.0, 0.0, 1.0], "TaskName": "Rest"}';
3030
validate.JSON(file, jsonObj, function (issues) {
3131
assert(issues.length === 1 && issues[0].code == 55);
3232
});
3333
});
34+
35+
var meg_file = {
36+
name: 'sub-01_run-01_meg.json',
37+
relativePath: '/sub-01_run-01_meg.json'
38+
};
39+
40+
it('*_meg.json sidecars should have required key/value pairs', function(){
41+
var jsonObj = '{"TaskName": "Audiovis", "SamplingFrequency": 1000, ' +
42+
' "PowerLineFrequency": 50, "DewarPosition": "Upright", ' +
43+
' "SoftwareFilters": "n/a", "DigitizedLandmarks": true,' +
44+
' "DigitizedHeadPoints": false}';
45+
validate.JSON(meg_file, jsonObj, function (issues) {
46+
assert(issues.length === 0);
47+
});
48+
49+
var jsonObjInval = jsonObj.replace(/"SamplingFrequency": 1000, /g, '');
50+
validate.JSON(meg_file, jsonObjInval, function(issues){
51+
assert(issues && issues.length === 1);
52+
});
53+
});
3454
});

tests/tsv.spec.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,35 @@ describe('TSV', function(){
167167
});
168168
});
169169

170+
// channels checks -----------------------------------------------------------------
171+
172+
var channelsFile = {
173+
name: 'sub-01_ses-meg_task-facerecognition_run-01_channels.tsv',
174+
relativePath: '/sub-01/ses-meg/meg/sub-01_ses-meg_task-facerecognition_run-01_channels.tsv'
175+
};
176+
177+
it("should not allow channels.tsv files without name column", function () {
178+
var tsv = 'header-one\ttype\tunits\n' +
179+
'value-one\tvalue-two\tvalue-three';
180+
validate.TSV.TSV(channelsFile, tsv, [], function (issues) {
181+
assert(issues.length === 1 && issues[0].code === 71);
182+
});
183+
});
184+
185+
it("should not allow channels.tsv files without type column", function () {
186+
var tsv = 'name\theader-two\tunits\n' +
187+
'value-one\tvalue-two\tvalue-three';
188+
validate.TSV.TSV(channelsFile, tsv, [], function (issues) {
189+
assert(issues.length === 1 && issues[0].code === 72);
190+
});
191+
});
192+
193+
194+
it("should allow channels.tsv files with name, type and units columns", function () {
195+
var tsv = 'name\ttype\tunits\theader-four\n' +
196+
'value-one\tvalue-two\tvalue-three\tvalue-four';
197+
validate.TSV.TSV(channelsFile, tsv, [], function (issues) {
198+
assert(issues.length === 0);
199+
});
200+
});
170201
});

tests/type.spec.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,42 @@ var suiteDWI = describe('utils.type.isDWI', function(){
204204
});
205205
});
206206

207+
208+
var suiteMEG = describe('utils.type.isMEG', function(){
209+
before(function(done) {
210+
var goodFilenames = [
211+
"/sub-01/ses-001/meg/sub-01_ses-001_task-rest_run-01_meg.json",
212+
"/sub-01/ses-001/meg/sub-01_ses-001_task-rest_run-01_part-01_meg.fif",
213+
"/sub-01/ses-001/meg/sub-01_ses-001_task-rest_run-01_channels.tsv"
214+
];
215+
216+
goodFilenames.forEach(function (path) {
217+
suiteMEG.addTest(new Test("isMeg('" + path + "') === true", function (isdone){
218+
assert.equal(utils.type.isMeg(path), true);
219+
isdone();
220+
}));
221+
});
222+
223+
var badFilenames = [
224+
"/sub-01/meg/sub-01_ses-001_task-rest_run-01_meg.json",
225+
"/sub-01/ses-001/meg/sub-12_ses-001_task-rest_run-01_part-01_meg.fif",
226+
"/sub-01/ses-001/meg/sub-01_ses-001_task-rest_run-01_meg.tsv"];
227+
228+
badFilenames.forEach(function (path) {
229+
suiteMEG.addTest(new Test("isMeg('" + path + "') === false", function (isdone){
230+
assert.equal(utils.type.isMeg(path), false);
231+
isdone();
232+
}));
233+
});
234+
done();
235+
});
236+
237+
// we need to have at least one non-dynamic test
238+
return it('dummy test', function() {
239+
require('assert').ok(true);
240+
});
241+
});
242+
207243
describe('utils.type.isPhenotypic', function () {
208244
it('should allow .tsv and .json files in the /phenotype directory', function () {
209245
assert(utils.type.isPhenotypic('/phenotype/acds_adult.json'));

utils/issues/list.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,5 +346,25 @@ module.exports = {
346346
key: 'FILENAME_COLUMN',
347347
severity: 'error',
348348
reason: "_scans.tsv files must have a 'filename' column."
349+
},
350+
70: {
351+
key: 'WRONG_NEW_LINE',
352+
severity: 'error',
353+
reason: "All TSV files must use Line Feed '\\n' characters to denote new lines. This files uses Carriage Return '\\r'."
354+
},
355+
71: {
356+
key: 'CHANNELS_COLUMN_NAME',
357+
severity: 'error',
358+
reason: "First column of the channels file must be named 'name'"
359+
},
360+
72: {
361+
key: 'CHANNELS_COLUMN_TYPE',
362+
severity: 'error',
363+
reason: "Second column of the channels file must be named 'type'"
364+
},
365+
73: {
366+
key: 'CHANNELS_COLUMN_UNITS',
367+
severity: 'error',
368+
reason: "Third column of the channels file must be named 'units'"
349369
}
350370
};

utils/type.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ module.exports = {
2525
this.isAnat(path) ||
2626
this.isDWI(path) ||
2727
this.isFunc(path) ||
28+
this.isMeg(path) ||
2829
this.isBehavioral(path) ||
2930
this.isCont(path) ||
3031
this.isFieldMap(path) ||
@@ -50,11 +51,14 @@ module.exports = {
5051

5152
var multiDirFieldmapRe = new RegExp('^\\/(?:dir-[a-zA-Z0-9]+)_epi.json$');
5253

54+
var megTopRe = new RegExp('^\\/(?:ses-[a-zA-Z0-9]+_)?task-[a-zA-Z0-9]+(?:_acq-[a-zA-Z0-9]+)?(?:_proc-[a-zA-Z0-9]+)?'
55+
+ '(_meg.json|_channels.tsv|_photo.jpg|_coordsystem.json)$');
56+
5357
var otherTopFiles = new RegExp('^\\/(?:ses-[a-zA-Z0-9]+_)?(?:recording-[a-zA-Z0-9]+_)?(?:task-[a-zA-Z0-9]+_)?(?:acq-[a-zA-Z0-9]+_)?(?:rec-[a-zA-Z0-9]+_)?(?:run-[0-9]+_)?'
5458
+ '(physio.json|stim.json)$');
5559

5660
return (fixedTopLevelNames.indexOf(path) != -1 || funcTopRe.test(path) || dwiTopRe.test(path) ||
57-
anatTopRe.test(path) || multiDirFieldmapRe.test(path) || otherTopFiles.test(path));
61+
anatTopRe.test(path) || multiDirFieldmapRe.test(path) || otherTopFiles.test(path) || megTopRe.test(path));
5862
},
5963

6064
/**
@@ -101,8 +105,14 @@ module.exports = {
101105
'\\/)?\\1(_\\2)?(?:_acq-[a-zA-Z0-9]+)?(?:_rec-[a-zA-Z0-9]+)?(?:_run-[0-9]+)?(?:_)?'
102106
+ 'dwi.(?:json|bval|bvec)$');
103107

108+
var megSesRe = new RegExp('^\\/(sub-[a-zA-Z0-9]+)' +
109+
'\\/(?:(ses-[a-zA-Z0-9]+)' +
110+
'\\/)?\\1(_\\2)?(?:_task-[a-zA-Z0-9]+)?(?:_acq-[a-zA-Z0-9]+)?(?:_proc-[a-zA-Z0-9]+)?'
111+
+ '(_events.tsv|_channels.tsv|_meg.json|_coordsystem.json|_photo.jpg|_headshape.pos)$');
112+
104113
return conditionalMatch(scansRe, path) || conditionalMatch(funcSesRe, path) ||
105-
conditionalMatch(anatSesRe, path) || conditionalMatch(dwiSesRe, path);
114+
conditionalMatch(anatSesRe, path) || conditionalMatch(dwiSesRe, path) ||
115+
conditionalMatch(megSesRe, path);
106116
},
107117

108118
/**
@@ -178,7 +188,16 @@ module.exports = {
178188
return conditionalMatch(funcRe, path);
179189
},
180190

181-
isBehavioral: function (path) {
191+
isMeg: function(path) {
192+
var MegRe = new RegExp('^\\/(sub-[a-zA-Z0-9]+)' +
193+
'\\/(?:(ses-[a-zA-Z0-9]+)' +
194+
'\\/)?meg' +
195+
'\\/\\1(_\\2)?(?:_task-[a-zA-Z0-9]+)?(?:_acq-[a-zA-Z0-9]+)?(?:_run-[0-9]+)?(?:_proc-[a-zA-Z0-9]+)?(?:_part-[0-9]+)?' +
196+
'(_meg.(ctf|fif|fif.gz|4d|kit|kdf|itab)|(_meg.ds\\/.*)|(_events.tsv|_channels.tsv|_meg.json|_coordsystem.json|_photo.jpg|_headshape.pos))$');
197+
return conditionalMatch(MegRe, path);
198+
},
199+
200+
isBehavioral: function(path) {
182201
var funcBeh = new RegExp('^\\/(sub-[a-zA-Z0-9]+)' +
183202
'\\/(?:(ses-[a-zA-Z0-9]+)' +
184203
'\\/)?beh' +

validators/bids.js

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -112,18 +112,14 @@ BIDS = {
112112
if (path) {
113113
path = path.split('/');
114114
path = path.reverse();
115+
var isCorrectModality = false;
115116
if (
116-
path[0].includes('.nii') &&
117-
(
118-
path[1] == 'anat' ||
119-
path[1] == 'func' ||
120-
path[1] == 'dwi'
121-
) &&
122-
(
123-
(path[2] && path[2].indexOf('ses-') == 0) ||
124-
(path[2] && path[2].indexOf('sub-') == 0)
125-
)
126-
) {
117+
(path[0].includes('.nii') && ['anat', 'func', 'dwi'].indexOf(path[1]) !=-1 ) ||
118+
(path[0].includes('.json') && path[1] == 'meg')
119+
){
120+
isCorrectModality = true;
121+
}
122+
if (path[2] && (path[2].indexOf('ses-') == 0 || path[2].indexOf('sub-') == 0) && isCorrectModality){
127123
couldBeBIDS = true;
128124
break;
129125
}

validators/json.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ function checkUnits (file, sidecar) {
3737
schema = require('./schemas/bold.json');
3838
} else if (file.relativePath === "/dataset_description.json") {
3939
schema = require('./schemas/dataset_description.json');
40+
} else if (file.name.endsWith("meg.json")) {
41+
schema = require('./schemas/meg.json');
42+
} else if (file.name.endsWith("coordsystem.json")) {
43+
schema = require('./schemas/coordsystem.json');
4044
}
4145
if (schema) {
4246
var validate = ajv.compile(schema);

validators/schemas/bold.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
"CogAtlasID": {
66
"type": "string"
77
},
8+
"CogPOID": {
9+
"type": "string"
10+
},
811
"EchoTime": {
912
"type": "number"
1013
},
@@ -30,5 +33,10 @@
3033
"TaskName": {
3134
"type": "string"
3235
}
33-
}
34-
}
36+
},
37+
"required": [
38+
"TaskName",
39+
"RepetitionTime"
40+
]
41+
42+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"MEGCoordinateSystem": {"type": "string","minLength": 1},
5+
"MEGCoordinateUnits": {"type": "string","minLength": 1},
6+
"MEGCoordinateSystemDescription": {"type": "string"},
7+
"EEGCoordinateSystem": {"type": "string","minLength": 1},
8+
"EEGCoordinateUnits": {"type": "string","minLength": 1},
9+
"EEGCoordinateSystemDescription": {"type": "string"},
10+
"IntendedFor": {
11+
"anyOf": [
12+
{
13+
"type": "array",
14+
"items": {
15+
"type": "string",
16+
"minLength": 1
17+
}
18+
},
19+
{"type": "string", "minLength": 1}
20+
]
21+
},
22+
"FiducialsDescription": {"type": "string", "minLength": 1},
23+
"HeadCoilCoordinates": {"type": "object",
24+
"additionalProperties": {"type": "array"}
25+
},
26+
"HeadCoilCoordinateSystem": {"type": "string","minLength": 1},
27+
"HeadCoilCoordinateUnits": {"type": "string","minLength": 1},
28+
"HeadCoilCoordinateSystemDescription": {"type": "string"},
29+
"AnatomicalLandmarkCoordinates": {"type": "object",
30+
"additionalProperties": {"type": "array"}
31+
},
32+
"AnatomicalLandmarkCoordinateSystem": {"type": "string","minLength": 1},
33+
"AnatomicalLandmarkCoordinateUnits": {"type": "string","minLength": 1},
34+
"AnatomicalLandmarkCoordinateSystemDescription": {"type": "string"},
35+
"DigitizedHeadPoints": {"type": "string"},
36+
"DigitizedHeadPointsCoordinateSystem": {"type": "string"},
37+
"DigitizedHeadPointsCoordinateUnits": {"type": "string"},
38+
"DigitizedHeadPointsCoordinateSystemDescription": {"type": "string"}
39+
},
40+
"required": ["MEGCoordinateSystem",
41+
"MEGCoordinateUnits"
42+
],
43+
"additionalProperties": false
44+
}

0 commit comments

Comments
 (0)