diff --git a/lib/bids-matlab b/lib/bids-matlab index bda01141..d0815ea7 160000 --- a/lib/bids-matlab +++ b/lib/bids-matlab @@ -1 +1 @@ -Subproject commit bda011411defe36c00d1d501b6842fd677af32fc +Subproject commit d0815ea702ea85ba9348ab2780f84564fbd5520f diff --git a/tests/test_createDataDictionary.m b/manualTests/test_createDataDictionary.m similarity index 71% rename from tests/test_createDataDictionary.m rename to manualTests/test_createDataDictionary.m index cb3423ab..daef9e2e 100644 --- a/tests/test_createDataDictionary.m +++ b/manualTests/test_createDataDictionary.m @@ -25,14 +25,15 @@ function test_createDataDictionaryBasic() cfg = createFilename(cfg); + logFile.extraColumns = {'Speed', 'LHL24'}; + + logFile = saveEventsFile('init', cfg, logFile); + logFile.extraColumns.Speed.length = 1; logFile.extraColumns.LHL24.length = 3; - logFile = saveEventsFile('init', cfg, logFile); logFile = saveEventsFile('open', cfg, logFile); - createDataDictionary(cfg, logFile); - %% check that the file has the right path and name % data to test against @@ -53,16 +54,17 @@ function test_createDataDictionaryBasic() % test_createDataDictionary>test_createDataDictionaryBasic:48 % (/github/workspace/tests/test_createDataDictionary.m) - % actualStruct = bids.util.jsondecode(fullfile(funcDir, jsonFilename)); - % - % % data to test against - % expectedStruct = bids.util.jsondecode( ... - % fullfile(pwd, ... - % 'testData', ... - % 'eventsDataDictionary.json')); - % - % % test - % assertTrue(isequal(expectedStruct, actualStruct)); + actualStruct = bids.util.jsondecode(fullfile(funcDir, jsonFilename)); + + % data to test against + expectedStruct = bids.util.jsondecode( ... + fullfile( ... + pwd, ... + 'testData', ... + 'eventsDataDictionary.json')); + + % test + assertTrue(isequal(expectedStruct, actualStruct)); end @@ -85,6 +87,10 @@ function test_createDataDictionaryStim() cfg = createFilename(cfg); + stimLogFile.extraColumns = {'Speed', 'LHL24', 'is_Fixation'}; + + stimLogFile = saveEventsFile('init_stim', cfg, stimLogFile); + stimLogFile.extraColumns.Speed.length = 1; stimLogFile.extraColumns.LHL24.length = 3; stimLogFile.extraColumns.is_Fixation.length = 1; @@ -92,11 +98,7 @@ function test_createDataDictionaryStim() stimLogFile.SamplingFrequency = 100; stimLogFile.StartTime = 0; - stimLogFile = saveEventsFile('init', cfg, stimLogFile); - - stimLogFile = saveEventsFile('open_stim', cfg, stimLogFile); - - createDataDictionary(cfg, stimLogFile); + stimLogFile = saveEventsFile('open', cfg, stimLogFile); %% check that the file has the right path and name @@ -118,4 +120,16 @@ function test_createDataDictionaryStim() % test_createDataDictionary>test_createDataDictionaryBasic:48 % (/github/workspace/tests/test_createDataDictionary.m) + actualStruct = bids.util.jsondecode(fullfile(funcDir, jsonFilename)); + + % data to test against + expectedStruct = bids.util.jsondecode( ... + fullfile( ... + pwd, ... + 'testData', ... + 'stimDataDictionary.json')); + + % test + assertTrue(isequal(expectedStruct, actualStruct)); + end diff --git a/tests/test_createDatasetDescription.m b/manualTests/test_createDatasetDescription.m similarity index 100% rename from tests/test_createDatasetDescription.m rename to manualTests/test_createDatasetDescription.m diff --git a/manualTests/test_makeRawDataset.m b/manualTests/test_makeRawDataset.m index 1a0a74fe..71d7d67f 100644 --- a/manualTests/test_makeRawDataset.m +++ b/manualTests/test_makeRawDataset.m @@ -23,13 +23,17 @@ function test_makeRawDataset() cfg.task.name = 'testtask'; cfg.task.instructions = 'do this'; + + cfg.verbosity = 0; + + cfg = createFilename(cfg); logFile.extraColumns.Speed.length = 1; logFile.extraColumns.LHL24.length = 3; logFile.extraColumns.is_Fixation.length = 1; - cfg = createFilename(cfg); - + logFile = saveEventsFile('init', cfg, logFile); + extraInfo = struct('extraInfo', struct('nestedExtraInfo', 'something extra')); createJson(cfg, extraInfo); @@ -72,20 +76,21 @@ function test_makeRawDataset() % add dummy stim data stimLogFile.extraColumns.Speed.length = 1; - stimLogFile.extraColumns.LHL24.length = 3; + stimLogFile.extraColumns.LHL24.length = 1; stimLogFile.extraColumns.is_Fixation.length = 1; stimLogFile.SamplingFrequency = cfg.mri.repetitionTime; stimLogFile.StartTime = 0; - stimLogFile = saveEventsFile('open_stim', cfg, stimLogFile); + stimLogFile = saveEventsFile('init_stim', cfg, stimLogFile); + stimLogFile = saveEventsFile('open', cfg, stimLogFile); for i = 1:100 stimLogFile(i, 1).onset = cfg.mri.repetitionTime * i; stimLogFile(i, 1).trial_type = 'test'; stimLogFile(i, 1).duration = 1; stimLogFile(i, 1).Speed = rand(1); stimLogFile(i, 1).is_Fixation = rand > 0.5; - stimLogFile(i, 1).LHL24 = randn(1, 3); + stimLogFile(i, 1).LHL24 = randn(); end saveEventsFile('save', cfg, stimLogFile); saveEventsFile('close', cfg, stimLogFile); diff --git a/src/createDataDictionary.m b/src/createDataDictionary.m index def87e47..28d8d560 100644 --- a/src/createDataDictionary.m +++ b/src/createDataDictionary.m @@ -17,33 +17,23 @@ function createDataDictionary(cfg, logFile) fileName = strrep(logFile(1).filename, '.tsv', '.json'); fullFilename = getFullFilename(fileName, cfg); - jsonContent = setJsonContent(fullFilename, logFile); + jsonContent = setJsonContent(logFile); opts.Indent = ' '; + bids.util.jsonencode(fullFilename, jsonContent, opts); end -function jsonContent = setJsonContent(fullFilename, logFile) +function jsonContent = setJsonContent(logFile) - % transfer content of extra fields to json content - namesExtraColumns = returnNamesExtraColumns(logFile); + % regular _events file: add default _event file fields to the json content + if ~isfield(logFile, 'isStim') || isempty(logFile.isStim) || ~logFile.isStim - % default content for events file that will be overriddent if we are dealing - % with a stim file - jsonContent = struct( ... - 'onset', struct( ... - 'Description', 'time elapsed since experiment start', ... - 'Units', 's'), ... - 'trial_type', struct( ... - 'Description', 'types of trial', ... - 'Levels', ''), ... - 'duration', struct( ... - 'Description', 'duration of the event', ... - 'Units', 's') ... - ); - - if ismember('_stim', fullFilename) + jsonContent = logFile.columns; + + % _stim file: write stim-specific fields to the json content + elseif logFile.isStim samplingFrequency = nan; startTime = nan; @@ -59,9 +49,11 @@ function createDataDictionary(cfg, logFile) 'SamplingFrequency', samplingFrequency, ... 'StartTime', startTime, ... 'Columns', []); - end + % transfer content of extra fields to json content + namesExtraColumns = returnNamesExtraColumns(logFile); + for iExtraColumn = 1:numel(namesExtraColumns) nbCol = returnNbColumns(logFile, namesExtraColumns{iExtraColumn}); @@ -70,10 +62,8 @@ function createDataDictionary(cfg, logFile) headerName = returnHeaderName(namesExtraColumns{iExtraColumn}, nbCol, iCol); - if ismember('_stim', fullFilename) - + if isfield(logFile, 'isStim') && ~isempty(logFile.isStim) && logFile.isStim jsonContent.Columns{end + 1} = headerName; - end jsonContent.(headerName) = ... diff --git a/src/saveEventsFile.m b/src/saveEventsFile.m index dff76120..bf4dc656 100644 --- a/src/saveEventsFile.m +++ b/src/saveEventsFile.m @@ -72,12 +72,9 @@ % % - if nargin < 1 - error('Missing action input'); - end - if nargin < 2 - cfg = struct(); + error(['Missing arguments. Please specify ', ... + 'and as the first two arguments']); end if nargin < 3 || isempty(logFile) @@ -88,19 +85,35 @@ case 'init' - logFile = initializeExtraColumns(logFile); + % flag to indicate that this will be an _events file + logFile(1).isStim = false; - case 'open' + if isfield(cfg, 'fileName') && ... + isfield(cfg.fileName, 'events') && ... + ~isempty(cfg.fileName.events) + logFile(1).filename = cfg.fileName.events; + else + logFile(1).filename = ''; + end + logFile = initializeFile(logFile); - logFile(1).filename = cfg.fileName.events; + case 'init_stim' - logFile = initializeFile(cfg, logFile); + % flag to indicate that this will be an _stim file + logFile(1).isStim = true; - case 'open_stim' + if isfield(cfg, 'fileName') && ... + isfield(cfg.fileName, 'stim') && ... + ~isempty(cfg.fileName.stim) + logFile(1).filename = cfg.fileName.stim; + else + logFile(1).filename = ''; + end + logFile = initializeStimFile(logFile); - logFile(1).filename = cfg.fileName.stim; + case 'open' - logFile = initializeStimFile(cfg, logFile); + logFile = openFile(cfg, logFile); case 'save' @@ -163,30 +176,44 @@ end -function logFile = initializeFile(cfg, logFile) +function logFile = initializeFile(logFile) + % This function creates the bids field structure for json files for the + % three basic bids event columns, and for all requested extra columns. + % + % Note that subfields (e.g. unit, levels etc. can be changed by the user + % before calling openFile. + + % initialize holy trinity (onset, trial_type, duration) columns + logFile(1).columns = struct( ... + 'onset', struct( ... + 'Description', ... + 'time elapsed since experiment start', ... + 'Units', 's'), ... + 'trial_type', struct( ... + 'Description', 'types of trial', ... + 'Levels', ''), ... + 'duration', struct( ... + 'Description', ... + 'duration of the event or the block', ... + 'Units', 's') ... + ); - logFile = initializeStimFile(cfg, logFile); + logFile = initializeExtraColumns(logFile); - % print the basic BIDS columns - fprintf(logFile(1).fileID, '%s\t%s\t%s', 'onset', 'duration', 'trial_type'); - fprintf(1, '%s\t%s\t%s', 'onset', 'duration', 'trial_type'); +end - printHeaderExtraColumns(logFile); +function logFile = initializeStimFile(logFile) - % next line so we start printing at the right place - fprintf(logFile(1).fileID, '\n'); - fprintf(1, '\n'); + logFile = initializeExtraColumns(logFile); end -function logFile = initializeStimFile(cfg, logFile) - - logFile = initializeExtraColumns(logFile); +function logFile = openFile(cfg, logFile) createDataDictionary(cfg, logFile); % Initialize txt logfiles and empty fields for the standard BIDS - % event file + % event file logFile(1).fileID = fopen( ... fullfile( ... cfg.dir.outputSubject, ... @@ -194,6 +221,22 @@ logFile.filename), ... 'w'); + if ~logFile(1).isStim + % print the basic BIDS columns + fprintf(logFile(1).fileID, '%s\t%s\t%s', 'onset', 'duration', 'trial_type'); + fprintf(1, '%s\t%s\t%s', 'onset', 'duration', 'trial_type'); + + printHeaderExtraColumns(logFile); + + % next line so we start printing at the right place + fprintf(logFile(1).fileID, '\n'); + fprintf(1, '\n'); + + elseif logFile(1).isStim + % don't print column headers for _stim.tsv + + end + end function printHeaderExtraColumns(logFile) @@ -307,25 +350,71 @@ function printHeaderExtraColumns(logFile) logFile = checklLogFile('fields', logFile, iEvent, cfg); - onset = logFile(iEvent).onset; - duration = logFile(iEvent).duration; - trial_type = logFile(iEvent).trial_type; + % check if this event should be skipped + skipEvent = false; + + % if this is _events file, we skip events with onset or duration + % that are empty, nan or char. + if ~logFile(1).isStim + + onset = logFile(iEvent).onset; + duration = logFile(iEvent).duration; + trial_type = logFile(iEvent).trial_type; + + if any(cell2mat(cellfun(@isnan, {onset duration}, 'UniformOutput', false))) || ... + any(cellfun(@ischar, {onset duration})) || ... + any(isempty({onset duration})) + + skipEvent = true; + + warningMessageID = 'saveEventsFile:emptyEvent'; + warningMessage = sprintf(['Skipping saving this event. \n '... + 'onset: %s \n duration: %s \n'], ... + onset, ... + duration); + end + + % if this is _stim file, we skip missing events (i.e. events where + % all extra columns have NO values) + elseif logFile(1).isStim + + namesExtraColumns = returnNamesExtraColumns(logFile); + isValid = ones(1, numel(namesExtraColumns)); + for iExtraColumn = 1:numel(namesExtraColumns) + data = logFile(iEvent).(namesExtraColumns{iExtraColumn}); + if isempty(data) || all(isnan(data)) || (ischar(data) && strcmp(data, 'n/a')) + isValid(iExtraColumn) = 0; + end + end + if all(~isValid) + skipEvent = true; + + warningMessageID = 'saveEventsFile:emptyEvent'; + warningMessage = sprintf(['Skipping saving this event. \n', ... + 'No values defined. \n']); + elseif any(~isValid) + skipEvent = false; + + warningMessageID = 'saveEventsFile:missingData'; + warningMessage = sprintf('Missing some %s data for this event. \n', ... + namesExtraColumns{find(isValid)}); + end + end - % we skip events with onset or duration that are empty, nan or char - if any(cell2mat(cellfun(@isnan, {onset duration}, 'UniformOutput', false))) || ... - any(cellfun(@ischar, {onset duration})) || ... - any(isempty({onset duration})) + % now save the event to log file (if not skipping) + if skipEvent - warning('saveEventsFile:emptyEvent', ... - '\nSkipping saving this event.\n onset: %s \n duration: %s\n', ... - onset, ... - duration); + warning(warningMessageID, warningMessage); else - printData(logFile(1).fileID, onset, cfg); - printData(logFile(1).fileID, duration, cfg); - printData(logFile(1).fileID, trial_type, cfg); + if ~logFile(1).isStim + + printData(logFile(1).fileID, onset, cfg); + printData(logFile(1).fileID, duration, cfg); + printData(logFile(1).fileID, trial_type, cfg); + + end printExtraColumns(logFile, iEvent, cfg); @@ -333,6 +422,7 @@ function printHeaderExtraColumns(logFile) fprintf(1, '\n'); end + end end @@ -383,6 +473,9 @@ function printData(output, data, cfg) logFile(2:end) = []; namesColumns = {'onset', 'duration', 'trial_type'}; + if logFile(1).isStim + namesColumns = {}; + end namesExtraColumns = returnNamesExtraColumns(logFile); namesColumns = cat(2, namesColumns, namesExtraColumns'); diff --git a/src/utils/utilsForTests/setUp.m b/src/utils/utilsForTests/setUp.m index fa29d4f4..0f7cf9eb 100644 --- a/src/utils/utilsForTests/setUp.m +++ b/src/utils/utilsForTests/setUp.m @@ -13,6 +13,10 @@ cfg = createFilename(cfg); + logFile.extraColumns = {'Speed', 'LHL24', 'is_Fixation'}; + + logFile = saveEventsFile('init', cfg, logFile); + logFile.extraColumns.Speed.length = 1; logFile.extraColumns.LHL24.length = 12; logFile.extraColumns.is_Fixation.length = 1; diff --git a/tests/testData/stimDataDictionary.json b/tests/testData/stimDataDictionary.json new file mode 100644 index 00000000..39490c72 --- /dev/null +++ b/tests/testData/stimDataDictionary.json @@ -0,0 +1,46 @@ +{ + "SamplingFrequency": 100, + "StartTime": 0, + "Columns": [ + "Speed", + "LHL24_01", + "LHL24_02", + "LHL24_03", + "is_Fixation" + ], + "Speed": { + "Description": "", + "Levels": "", + "LongName": "", + "TermURL": "", + "Units": "" + }, + "LHL24_01": { + "Description": "", + "Levels": "", + "LongName": "", + "TermURL": "", + "Units": "" + }, + "LHL24_02": { + "Description": "", + "Levels": "", + "LongName": "", + "TermURL": "", + "Units": "" + }, + "LHL24_03": { + "Description": "", + "Levels": "", + "LongName": "", + "TermURL": "", + "Units": "" + }, + "is_Fixation": { + "Description": "", + "Levels": "", + "LongName": "", + "TermURL": "", + "Units": "" + } +} \ No newline at end of file diff --git a/tests/test_createQuestionList.m b/tests/test_createQuestionList.m index a37dd6cb..fce8587c 100644 --- a/tests/test_createQuestionList.m +++ b/tests/test_createQuestionList.m @@ -23,7 +23,6 @@ function test_createQuestionListBasic() end - function test_createQuestionListRestricted() %% set up diff --git a/tests/test_readAndFilterLogfile.m b/tests/test_readAndFilterLogfile.m index 81406ed5..ab2c49c7 100644 --- a/tests/test_readAndFilterLogfile.m +++ b/tests/test_readAndFilterLogfile.m @@ -9,8 +9,8 @@ function test_readAndFilterLogfileBasic() %% set up - cfg.dir.output = fullfile(fileparts(mfilename('fullpath')), '..', 'output'); [cfg, logFile] = setUp(); + cfg.dir.output = fullfile(fileparts(mfilename('fullpath')), '..', 'output'); % create the events file and header logFile = saveEventsFile('open', cfg, logFile); diff --git a/tests/test_saveEventsFileInit.m b/tests/test_saveEventsFileInit.m index ebc077ac..ff4aac42 100644 --- a/tests/test_saveEventsFileInit.m +++ b/tests/test_saveEventsFileInit.m @@ -15,14 +15,24 @@ function test_saveEventsFileInitBasic() % make sure the dependencies are there checkCFG(cfg); - [logFile] = saveEventsFile('init'); + [logFile] = saveEventsFile('init', cfg); %% data to test against expectedStrcut(1).filename = ''; expectedStrcut(1).extraColumns = []; + expectedStrcut(1).isStim = false; + + expectedStrcut(1).columns.onset.Description = 'time elapsed since experiment start'; + expectedStrcut(1).columns.onset.Units = 's'; + + expectedStrcut(1).columns.trial_type.Description = 'types of trial'; + expectedStrcut(1).columns.trial_type.Levels = ''; + + expectedStrcut(1).columns.duration.Description = 'duration of the event or the block'; + expectedStrcut(1).columns.duration.Units = 's'; %% test - assertEqual(expectedStrcut, logFile); + assertTrue(isequal(expectedStrcut, logFile)); end @@ -39,6 +49,7 @@ function test_saveEventsFileInitExtraColumns() [logFile] = saveEventsFile('init', cfg, logFile); %% data to test against + expectedStrcut = saveEventsFile('init', cfg); expectedStrcut(1).extraColumns.Speed.length = 1; expectedStrcut(1).extraColumns.Speed.bids.LongName = ''; expectedStrcut(1).extraColumns.Speed.bids.Description = ''; @@ -65,6 +76,7 @@ function test_saveEventsFileInitExtraColumnsArray() [logFile] = saveEventsFile('init', cfg, logFile); %% data to test against + expectedStrcut = saveEventsFile('init', cfg); expectedStrcut(1).extraColumns.Speed.length = 1; expectedStrcut(1).extraColumns.Speed.bids.LongName = ''; expectedStrcut(1).extraColumns.Speed.bids.Description = ''; diff --git a/tests/test_saveEventsFileOpen.m b/tests/test_saveEventsFileOpen.m index be04d199..6935ab92 100644 --- a/tests/test_saveEventsFileOpen.m +++ b/tests/test_saveEventsFileOpen.m @@ -25,8 +25,10 @@ function test_saveEventsFileOpenBasic() cfg = createFilename(cfg); + logFile = saveEventsFile('init', cfg); + % create the events file and header - logFile = saveEventsFile('open', cfg); + logFile = saveEventsFile('open', cfg, logFile); % close the file saveEventsFile('close', cfg, logFile); @@ -70,8 +72,10 @@ function test_saveEventsFileOpenStimfile() cfg = createFilename(cfg); + logFile = saveEventsFile('init_stim', cfg); + % create the events file and header - logFile = saveEventsFile('open_stim', cfg); + logFile = saveEventsFile('open', cfg, logFile); % close the file saveEventsFile('close', cfg, logFile); @@ -111,6 +115,8 @@ function test_saveEventsFileOpenExtraColumns() % they will be added to the tsv files in the order the user input them logFile.extraColumns = {'Speed', 'is_Fixation'}; + logFile = saveEventsFile('init', cfg, logFile); + % create the events file and header logFile = saveEventsFile('open', cfg, logFile); diff --git a/tests/test_saveEventsFileOpenMultiColumn.m b/tests/test_saveEventsFileOpenMultiColumn.m index 05e63f41..89864533 100644 --- a/tests/test_saveEventsFileOpenMultiColumn.m +++ b/tests/test_saveEventsFileOpenMultiColumn.m @@ -25,11 +25,15 @@ function test_saveEventsFileOpenMultiColumnCheckHeader() cfg = createFilename(cfg); - % define the extra columns: here we specify how many columns we want for - % each variable + % define the extra columns names + logFile.extraColumns = {'Speed', 'LHL24', 'is_Fixation'}; + + % initalize logfile + logFile = saveEventsFile('init', cfg, logFile); + + % extra columns: here we specify how many columns we want for each variable logFile.extraColumns.Speed.length = 1; % will set 1 columns with name Speed logFile.extraColumns.LHL24.length = 12; % will set 12 columns with names LHL24-01, LHL24-02, ... - logFile.extraColumns.is_Fixation = []; % will set 1 columns with name is_Fixation % create the events file and header logFile = saveEventsFile('open', cfg, logFile);