From 15207f66cbd3399a9d8469cc8c895ca28a51e018 Mon Sep 17 00:00:00 2001 From: Edouard Date: Sat, 15 Aug 2026 16:52:58 -0400 Subject: [PATCH 01/14] Improve robustness to wrongly formated files --- toolbox/io/in_data_snirf.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/toolbox/io/in_data_snirf.m b/toolbox/io/in_data_snirf.m index cbb7f1131b..57148cf22d 100644 --- a/toolbox/io/in_data_snirf.m +++ b/toolbox/io/in_data_snirf.m @@ -155,7 +155,7 @@ jnirs.nirs.probe.detectorLabels = {}; end % Events. Convert cell array to struct array - if iscell(jnirs.nirs.stim) + if isfield(jnirs.nirs,'stim') && iscell(jnirs.nirs.stim) jnirs.nirs.stim = cell2mat(jnirs.nirs.stim); end @@ -555,6 +555,7 @@ error_list = {}; for iAux = 1:nAux if ~isempty(jnirs.nirs.data.dataTimeSeries) && ~isempty(jnirs.nirs.aux(iAux).dataTimeSeries) ... + && isfield(jnirs.nirs.aux(iAux), 'time') ... && length(jnirs.nirs.data.time) == length(jnirs.nirs.aux(iAux).time) ... && isequal(expendTime(jnirs.nirs.data.time, nSample), expendTime(jnirs.nirs.aux(iAux).time,nSample)) ... && ( ~isfield(jnirs.nirs.aux(iAux), 'timeOffset') || jnirs.nirs.aux(iAux).timeOffset == 0) From 70b8205d86412da1b7d23774aec3bb08797b3a2c Mon Sep 17 00:00:00 2001 From: Edouard Date: Sat, 15 Aug 2026 17:39:56 -0400 Subject: [PATCH 02/14] in_tsv return an helper struct for easier indexing to the table --- toolbox/io/in_tsv.m | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/toolbox/io/in_tsv.m b/toolbox/io/in_tsv.m index e04c7a9ece..16581664c6 100644 --- a/toolbox/io/in_tsv.m +++ b/toolbox/io/in_tsv.m @@ -1,7 +1,7 @@ -function [cTsv, ColNames] = in_tsv(TsvFile, ColNames, isWarning, Delimiter, isDecimalComma) +function [cTsv, ColNames, index] = in_tsv(TsvFile, ColNames, isWarning, Delimiter, isDecimalComma) % IN_TSV: Reads specific columns in a .tsv file % -% USAGE: [cTsv, ColNames] = in_tsv(TsvFile, ColNames=[all], isWarning=1, Delimiter=[tab], isDecimalComma=0) +% USAGE: [cTsv, ColNames, index] = in_tsv(TsvFile, ColNames=[all], isWarning=1, Delimiter=[tab], isDecimalComma=0) % % INPUTS: % - TsvFile : Full path to input filename @@ -11,7 +11,12 @@ % - isWarning : If 1, display a warning for each missing column name (only if ColNames is set) % - Delimiter : Character (eg. sprintf('\t') or ';) % - isDecimalComma : If 1, read the entire file, replace all the commas with dots, and then scan it - +% OUTPULS: +% - cTsv: table value. +% - ColNames: {1 x Ncol} Cell-array of strings (eg. {'col1', 'col2'}) +% - Index: Struct, making the link between the ColNames and the columns +% of cTsv. Example: [cTsv, ColNames, index] = in_tsv(TsvFile, {'col1', +% 'col2'}): index.col1 = 1 so, cTsv can be read using: cTsv(:, index.col1) % @============================================================================= % This function is part of the Brainstorm software: % https://neuroimage.usc.edu/brainstorm @@ -54,7 +59,9 @@ ColNames = ColNames(1,:); end % Intialize returned variable -cTsv = {}; +cTsv = {}; +index = create_index(ColNames); + % Open file fid = fopen(TsvFile, 'r'); if (fid < 0) @@ -131,4 +138,12 @@ end end end +end + +function index = create_index(ColNames) + index = struct(); + for jCol = 1:length(ColNames) + index.(ColNames{jCol}) = jCol; + end +end From 3dda436bd30d6ed638c38c714439c596431a6216 Mon Sep 17 00:00:00 2001 From: Edouard Date: Sat, 15 Aug 2026 17:41:12 -0400 Subject: [PATCH 03/14] Improve finding of the optodes file --- toolbox/io/in_channel_bids_nirs.m | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/toolbox/io/in_channel_bids_nirs.m b/toolbox/io/in_channel_bids_nirs.m index b0910c4599..98897b36c2 100644 --- a/toolbox/io/in_channel_bids_nirs.m +++ b/toolbox/io/in_channel_bids_nirs.m @@ -34,9 +34,9 @@ return; end - OptodeFile = strrep(ChannelFile, '_channels.tsv', '_optodes.tsv'); - if exist(OptodeFile, 'file') - tsvOptodes = in_tsv(OptodeFile, {'name','type', 'x', 'y', 'z', 'template_x', 'template_y', 'template_z'}, 0); + OptodeFile = find_optodes_file(ChannelFile); + if ~isempty(OptodeFile) && exist(OptodeFile, 'file') + [tsvOptodes, ~, OptodesIndex] = in_tsv(OptodeFile, {'name', 'type', 'x', 'y', 'z', 'template_x', 'template_y', 'template_z'}); else tsvOptodes = {}; end @@ -101,7 +101,24 @@ end +function optodes_path = find_optodes_file(channels_path) + % FIND_OPTODES_FILE Given a BIDS channels.tsv path, find the matching + % optodes.tsv file. + % + % See https://bids-specification.readthedocs.io/en/stable/modality-specific-files/near-infrared-spectroscopy.html#nirs-recording-data + [folder, name, ~] = fileparts(channels_path); + optodes_path = file_find(folder, '*_optodes.tsv', 1, 0); + + % If there is no ambiguity, we can return the file + if length(optodes_path) <= 1 + optodes_path = optodes_path{1}; + return + end + + error('Todo'); + +end function chann_name = parse_name(name) From 2edc7b86f8a24113a45017e08efa512710d1f5b9 Mon Sep 17 00:00:00 2001 From: Edouard Date: Sat, 15 Aug 2026 17:41:28 -0400 Subject: [PATCH 04/14] correctly read the coordinate from the file --- toolbox/io/in_channel_bids_nirs.m | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/toolbox/io/in_channel_bids_nirs.m b/toolbox/io/in_channel_bids_nirs.m index 98897b36c2..07ea69bc8a 100644 --- a/toolbox/io/in_channel_bids_nirs.m +++ b/toolbox/io/in_channel_bids_nirs.m @@ -93,7 +93,7 @@ end if ~isempty(tsvOptodes) - ChannelMat.Channel(iChannel).Loc = getOptodesCoordinate(tsvOptodes, tsvValues{iChannel,3}, tsvValues{iChannel,4}); + ChannelMat.Channel(iChannel).Loc = getOptodesCoordinate(OptodesIndex, tsvOptodes, tsvValues{iChannel,3}, tsvValues{iChannel,4}); end end @@ -141,7 +141,7 @@ channel_name = strrep(channel_name, 'Rx','D'); end -function coordinates = getOptodesCoordinate(tsvOptodes, sourceName, detectorName) +function coordinates = getOptodesCoordinate(OptodesIndex, tsvOptodes, sourceName, detectorName) coordinates = []; if strcmp(detectorName, 'n/a') || strcmp(sourceName, 'n/a') @@ -149,37 +149,37 @@ end % Read optodes coordinate - iSource = find(strcmp(tsvOptodes(:,1), sourceName)); + iSource = find(strcmp(tsvOptodes(:, OptodesIndex.name), sourceName)); if isempty(iSource) warning('Unable to find source %s in optodes.tsv', sourceName) return; - elseif ~strcmp(tsvOptodes{iSource,2}, 'source') + elseif ~strcmp(tsvOptodes{iSource, OptodesIndex.type}, 'source') warning('%s should be a source but is labelled as a %s in optodes.tsv', sourceName, tsvOptodes{iSource,2}); return; end - if ~isempty(tsvOptodes{iSource,4}) && ~isempty(tsvOptodes{iSource,5}) && ~isempty(tsvOptodes{iSource,6}) - source_coord = [str2double(tsvOptodes{iSource,4}); str2double(tsvOptodes{iSource,5}); str2double(tsvOptodes{iSource,6})]; - elseif ~isempty(tsvOptodes{iSource,7}) && ~isempty(tsvOptodes{iSource,8}) && ~isempty(tsvOptodes{iSource,9}) - source_coord = [str2double(tsvOptodes{iSource,7}); str2double(tsvOptodes{iSource,8}); str2double(tsvOptodes{iSource,9})]; + if ~isempty(tsvOptodes{iSource, OptodesIndex.x}) && ~isempty(tsvOptodes{iSource, OptodesIndex.y}) && ~isempty(tsvOptodes{iSource,OptodesIndex.z}) + source_coord = [str2double(tsvOptodes{iSource, OptodesIndex.x}); str2double(tsvOptodes{iSource, OptodesIndex.y}); str2double(tsvOptodes{iSource, OptodesIndex.z})]; + elseif ~isempty(tsvOptodes{iSource, OptodesIndex.template_x}) && ~isempty(tsvOptodes{iSource, OptodesIndex.template_y}) && ~isempty(tsvOptodes{iSource, OptodesIndex.template_z}) + source_coord = [str2double(tsvOptodes{iSource, OptodesIndex.template_x}); str2double(tsvOptodes{iSource, OptodesIndex.template_y}); str2double(tsvOptodes{iSource, OptodesIndex.template_z})]; else warning('No coordinate available for %s in optodes.tsv', sourceName) return; end - iDetector = find(strcmp(tsvOptodes(:,1), detectorName)); + iDetector = find(strcmp(tsvOptodes(:, OptodesIndex.name), detectorName)); if isempty(iDetector) warning('Unable to find detector %s in optodes.tsv', detectorName) return; - elseif ~strcmp(tsvOptodes{iDetector,2}, 'detector') - warning('%s should be a detector but is labelled as a %s in optodes.tsv', detectorName, tsvOptodes{iSource,2}); + elseif ~strcmp(tsvOptodes{iDetector, OptodesIndex.type}, 'detector') + warning('%s should be a detector but is labelled as a %s in optodes.tsv', detectorName, tsvOptodes{iSource, OptodesIndex.type}); return; end - if ~isempty(tsvOptodes{iDetector,4}) && ~isempty(tsvOptodes{iDetector,5}) && ~isempty(tsvOptodes{iDetector,6}) - detector_coord = [str2double(tsvOptodes{iDetector,4}) ; str2double(tsvOptodes{iDetector,5}); str2double(tsvOptodes{iDetector,6})]; - elseif ~isempty(tsvOptodes{iDetector,7}) && ~isempty(tsvOptodes{iDetector,8}) && ~isempty(tsvOptodes{iDetector,9}) - detector_coord = [str2double(tsvOptodes{iDetector,7}) ; str2double(tsvOptodes{iDetector,8}); str2double(tsvOptodes{iDetector,9})]; + if ~isempty(tsvOptodes{iDetector, OptodesIndex.x}) && ~isempty(tsvOptodes{iDetector, OptodesIndex.y}) && ~isempty(tsvOptodes{iDetector, OptodesIndex.z}) + detector_coord = [str2double(tsvOptodes{iDetector, OptodesIndex.x}) ; str2double(tsvOptodes{iDetector, OptodesIndex.y}); str2double(tsvOptodes{iDetector, OptodesIndex.z})]; + elseif ~isempty(tsvOptodes{iDetector,OptodesIndex.template_x}) && ~isempty(tsvOptodes{iDetector, OptodesIndex.template_y}) && ~isempty(tsvOptodes{iDetector, OptodesIndex.template_z}) + detector_coord = [str2double(tsvOptodes{iDetector, OptodesIndex.template_x}) ; str2double(tsvOptodes{iDetector, OptodesIndex.template_y}); str2double(tsvOptodes{iDetector, OptodesIndex.template_z})]; else warning('No coordinate available for %s in optodes.tsv', detectorName) return; From fe22d54f95de100df9ebd61593abcc60d58f7cde Mon Sep 17 00:00:00 2001 From: Edouard Date: Sat, 15 Aug 2026 17:51:14 -0400 Subject: [PATCH 05/14] clean code --- toolbox/io/in_channel_bids_nirs.m | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/toolbox/io/in_channel_bids_nirs.m b/toolbox/io/in_channel_bids_nirs.m index 07ea69bc8a..0507c19c46 100644 --- a/toolbox/io/in_channel_bids_nirs.m +++ b/toolbox/io/in_channel_bids_nirs.m @@ -24,11 +24,11 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Edouard Delaire, 2025 +% Authors: Edouard Delaire, 2025-2026 % Read the TSV file - tsvValues = in_tsv(ChannelFile, {'name', 'type', 'source', 'detector', 'wavelength_nominal', 'status', 'component'}, 0); - if isempty(tsvValues) || isempty(tsvValues{1,1}) + [channelValue, ~, channelIndex] = in_tsv(ChannelFile, {'name', 'type', 'source', 'detector', 'wavelength_nominal', 'status', 'component'}, 0); + if isempty(channelValue) || isempty(channelValue{1, channelIndex.name}) disp('BIDS> Error: Invalid _channels.tsv file.'); ChannelMat = []; return; @@ -41,7 +41,7 @@ tsvOptodes = {}; end - nChan = size(tsvValues,1); + nChan = size(channelValue,1); % Initialize returned structure ChannelMat = db_template('channelmat'); @@ -53,19 +53,20 @@ for iChannel = 1:nChan - channel_type = upper(tsvValues{iChannel,2}); + channel_type = upper(channelValue{iChannel, channelIndex.type}); if any(strcmp(channel_type, {'NIRSCWAMPLITUDE', 'NIRSCWOPTICALDENSITY', 'NIRSCWHBO', 'NIRSCWHBR'})) - channel_name = parse_name(tsvValues{iChannel,1}); + channel_name = parse_name(channelValue{iChannel, channelIndex.name}); else - channel_name = tsvValues{iChannel,1}; + channel_name = channelValue{iChannel, channelIndex.name}; end switch(channel_type) case {'NIRSCWAMPLITUDE', 'NIRSCWOPTICALDENSITY'} - ChannelMat.Channel(iChannel).Name = sprintf('%sWL%d', channel_name, str2double(tsvValues{iChannel,5})); + wl = round(str2double(channelValue{iChannel, channelIndex.wavelength_nominal})); + ChannelMat.Channel(iChannel).Name = sprintf('%sWL%d', channel_name, wl); ChannelMat.Channel(iChannel).Type = 'NIRS'; - ChannelMat.Channel(iChannel).Group = sprintf('WL%d', str2double(tsvValues{iChannel,5})); + ChannelMat.Channel(iChannel).Group = sprintf('WL%d', wl); ChannelMat.Channel(iChannel).Weight = 1; case {'NIRSCWHBO', 'NIRSCWHBR'} ChannelMat.Channel(iChannel).Name = sprintf('%sHb%s', channel_name, channel_type(end)); @@ -73,11 +74,11 @@ ChannelMat.Channel(iChannel).Group = sprintf('Hb%s', channel_type(end)); ChannelMat.Channel(iChannel).Weight = 1; case {'ACCEL', 'GYRO', 'MAGN'} - if isempty(tsvValues{iChannel,7}) + if isempty(channelValue{iChannel,7}) error('Componnent for channel %s is not defnied', channel_name) end - ChannelMat.Channel(iChannel).Name = sprintf('%s_%s', channel_name, tsvValues{iChannel,7}); + ChannelMat.Channel(iChannel).Name = sprintf('%s_%s', channel_name, channelValue{iChannel, channelIndex.component}); ChannelMat.Channel(iChannel).Type = 'Misc'; % Is there a better type ? ChannelMat.Channel(iChannel).Group = []; ChannelMat.Channel(iChannel).Weight = 1; @@ -88,12 +89,12 @@ ChannelMat.Channel(iChannel).Weight = 1; otherwise isValidChannel(iChannel) = false; - warning('Unsoprted channel %s with type %s', tsvValues{iChannel,1}, tsvValues{iChannel,2} ) + warning('Unsoprted channel %s with type %s', channelValue{iChannel, channelIndex.name}, channelValue{iChannel, channelIndex.type} ) continue; end if ~isempty(tsvOptodes) - ChannelMat.Channel(iChannel).Loc = getOptodesCoordinate(OptodesIndex, tsvOptodes, tsvValues{iChannel,3}, tsvValues{iChannel,4}); + ChannelMat.Channel(iChannel).Loc = getOptodesCoordinate(OptodesIndex, tsvOptodes, channelValue{iChannel, channelIndex.source}, channelValue{iChannel, channelIndex.detector}); end end From 829fc5f779d969e53bbb86067e8f953631106411 Mon Sep 17 00:00:00 2001 From: Edouard Date: Sat, 15 Aug 2026 17:55:59 -0400 Subject: [PATCH 06/14] return channel status --- toolbox/io/in_channel_bids_nirs.m | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/toolbox/io/in_channel_bids_nirs.m b/toolbox/io/in_channel_bids_nirs.m index 0507c19c46..212a44c196 100644 --- a/toolbox/io/in_channel_bids_nirs.m +++ b/toolbox/io/in_channel_bids_nirs.m @@ -1,11 +1,13 @@ -function ChannelMat = in_channel_bids_nirs(ChannelFile) +function [ChannelMat, ChannelStatus] = in_channel_bids_nirs(ChannelFile) % IN_CHANNEL_BIDS_NIRS: Read NIRS channels file from a BIDS _channels.tsv file. % % USAGE: ChannelMat = in_channel_bids_nirs(ChannelFile) % % INPUTS: % - ChannelFile : Full path to the .tsv file - +% - ChannelStatus : Vector (1xnChannel): +% ChannelStatus(i) == 1 if the channel i is good, -1 otherwise. + % @============================================================================= % This function is part of the Brainstorm software: % https://neuroimage.usc.edu/brainstorm @@ -30,7 +32,7 @@ [channelValue, ~, channelIndex] = in_tsv(ChannelFile, {'name', 'type', 'source', 'detector', 'wavelength_nominal', 'status', 'component'}, 0); if isempty(channelValue) || isempty(channelValue{1, channelIndex.name}) disp('BIDS> Error: Invalid _channels.tsv file.'); - ChannelMat = []; + ChannelMat = []; ChannelStatus = []; return; end @@ -42,7 +44,8 @@ end nChan = size(channelValue,1); - + ChannelStatus = ones(1, nChan); + % Initialize returned structure ChannelMat = db_template('channelmat'); ChannelMat.Comment = 'BIDS channels'; @@ -60,7 +63,6 @@ channel_name = channelValue{iChannel, channelIndex.name}; end - switch(channel_type) case {'NIRSCWAMPLITUDE', 'NIRSCWOPTICALDENSITY'} wl = round(str2double(channelValue{iChannel, channelIndex.wavelength_nominal})); @@ -93,12 +95,19 @@ continue; end + if ~isempty(channelValue{iChannel, channelIndex.status}) && strcmpi(channelValue{iChannel, channelIndex.status}, 'bad') + ChannelStatus(iChannel) = -1; + end + + if ~isempty(tsvOptodes) ChannelMat.Channel(iChannel).Loc = getOptodesCoordinate(OptodesIndex, tsvOptodes, channelValue{iChannel, channelIndex.source}, channelValue{iChannel, channelIndex.detector}); end end - ChannelMat.Channel = ChannelMat.Channel(isValidChannel); + % Only keep supported channels + ChannelMat.Channel = ChannelMat.Channel(isValidChannel); + ChannelStatus = ChannelStatus(isValidChannel); end From 1136a597f47bdc901f5035bc48a42bb1749f4c44 Mon Sep 17 00:00:00 2001 From: Edouard Date: Sat, 15 Aug 2026 18:10:28 -0400 Subject: [PATCH 07/14] Apply fix to out_channel_bids_nirs --- toolbox/io/out_channel_bids_nirs.m | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/toolbox/io/out_channel_bids_nirs.m b/toolbox/io/out_channel_bids_nirs.m index 05ffb2d765..25a15d2ebe 100644 --- a/toolbox/io/out_channel_bids_nirs.m +++ b/toolbox/io/out_channel_bids_nirs.m @@ -1,4 +1,4 @@ -function out_channel_bids_nirs(BstChannelFile, OutputChannelFile, units, status) +function out_channel_bids_nirs(BstChannelFile, OutputChannelFile, file_units, status) % OUT_CHANNELK_BIDS_NIRS: Exports a Brainstorm NIRS channel file in an BIDS % _channels.tsv file. % @@ -28,6 +28,7 @@ function out_channel_bids_nirs(BstChannelFile, OutputChannelFile, units, status) % =============================================================================@ % % Authors: Jacob Busgang, 2025 +% Edouard Delaire, 2026 if nargin < 3 file_units = ''; @@ -53,7 +54,7 @@ function out_channel_bids_nirs(BstChannelFile, OutputChannelFile, units, status) Name = sprintf('S%s-D%s', tokens{1}{1}, tokens{1}{2}); if contains(channels(i).Group, 'WL') - if isempty(file_units) && ~(contains(file_units, {'unitless', 'OD', 'dOD'})) + if isempty(file_units) || ~(contains(file_units, {'unitless', 'OD', 'dOD'})) Type = 'NIRSCWAMPLITUDE'; WavelengthNominal = strrep(tokens{1}{3}, 'WL', ''); else @@ -98,7 +99,7 @@ function out_channel_bids_nirs(BstChannelFile, OutputChannelFile, units, status) end - if isempty(status) || status(i) + if isempty(status) || status(i) == 1 chan_status = 'good'; else chan_status = 'bad'; @@ -116,4 +117,3 @@ function out_channel_bids_nirs(BstChannelFile, OutputChannelFile, units, status) writetable(T, OutputChannelFile, 'FileType', 'text', 'Delimiter','\t' ); end - From cae8a57cd4af0d63e8d1e1b3aa4c53068289d481 Mon Sep 17 00:00:00 2001 From: Edouard Date: Sat, 15 Aug 2026 18:20:51 -0400 Subject: [PATCH 08/14] Allow to pass the optodes file as argument --- toolbox/io/in_channel_bids_nirs.m | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/toolbox/io/in_channel_bids_nirs.m b/toolbox/io/in_channel_bids_nirs.m index 212a44c196..ddd9ea66b1 100644 --- a/toolbox/io/in_channel_bids_nirs.m +++ b/toolbox/io/in_channel_bids_nirs.m @@ -1,11 +1,14 @@ -function [ChannelMat, ChannelStatus] = in_channel_bids_nirs(ChannelFile) +function [ChannelMat, ChannelStatus] = in_channel_bids_nirs(ChannelFile, OptodeFile) % IN_CHANNEL_BIDS_NIRS: Read NIRS channels file from a BIDS _channels.tsv file. % % USAGE: ChannelMat = in_channel_bids_nirs(ChannelFile) % % INPUTS: -% - ChannelFile : Full path to the .tsv file -% - ChannelStatus : Vector (1xnChannel): +% - ChannelFile : Full path to the _channels.tsv file +% - OptodeFile : Full path to the _optodes.tsv file +% OUTPUTS: +% - ChannelMat: Brainstorm matrix file structure +% - ChannelStatus : Vector (1xnChannel): % ChannelStatus(i) == 1 if the channel i is good, -1 otherwise. % @============================================================================= @@ -28,6 +31,10 @@ % % Authors: Edouard Delaire, 2025-2026 + if nargin < 2 || isempty(OptodeFile) + OptodeFile = find_optodes_file(ChannelFile); + end + % Read the TSV file [channelValue, ~, channelIndex] = in_tsv(ChannelFile, {'name', 'type', 'source', 'detector', 'wavelength_nominal', 'status', 'component'}, 0); if isempty(channelValue) || isempty(channelValue{1, channelIndex.name}) @@ -36,7 +43,6 @@ return; end - OptodeFile = find_optodes_file(ChannelFile); if ~isempty(OptodeFile) && exist(OptodeFile, 'file') [tsvOptodes, ~, OptodesIndex] = in_tsv(OptodeFile, {'name', 'type', 'x', 'y', 'z', 'template_x', 'template_y', 'template_z'}); else From 999504567150c94d9cd22cb26874e4f9a5044464 Mon Sep 17 00:00:00 2001 From: Edouard Date: Sat, 15 Aug 2026 18:52:22 -0400 Subject: [PATCH 09/14] minor fix --- toolbox/io/in_channel_bids_nirs.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolbox/io/in_channel_bids_nirs.m b/toolbox/io/in_channel_bids_nirs.m index ddd9ea66b1..d9c7501f6e 100644 --- a/toolbox/io/in_channel_bids_nirs.m +++ b/toolbox/io/in_channel_bids_nirs.m @@ -82,7 +82,7 @@ ChannelMat.Channel(iChannel).Group = sprintf('Hb%s', channel_type(end)); ChannelMat.Channel(iChannel).Weight = 1; case {'ACCEL', 'GYRO', 'MAGN'} - if isempty(channelValue{iChannel,7}) + if isempty(channelValue{iChannel, channelIndex.component}) error('Componnent for channel %s is not defnied', channel_name) end From fbbe63be9e57d31a43074e8036448e0943803b01 Mon Sep 17 00:00:00 2001 From: Edouard Date: Sun, 16 Aug 2026 12:36:04 -0400 Subject: [PATCH 10/14] Fix snirf output when there is one sample; or no event --- toolbox/io/in_fopen_bstmat.m | 6 +++++- toolbox/io/out_data_snirf.m | 11 ++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/toolbox/io/in_fopen_bstmat.m b/toolbox/io/in_fopen_bstmat.m index 5e6c59ab5f..52eebf5819 100644 --- a/toolbox/io/in_fopen_bstmat.m +++ b/toolbox/io/in_fopen_bstmat.m @@ -52,7 +52,11 @@ sFile.device = 'Brainstorm'; sFile.comment = DataMat(1).Comment; sFile.prop.times = [DataMat(1).Time(1), DataMat(1).Time(end)]; -sFile.prop.sfreq = 1 ./ (DataMat(1).Time(2) - DataMat(1).Time(1)); +if length(DataMat(1).Time) >= 2 + sFile.prop.sfreq = 1 ./ (DataMat(1).Time(2) - DataMat(1).Time(1)); +else + sFile.prop.sfreq = nan; +end sFile.prop.currCtfComp = 3; sFile.prop.destCtfComp = 3; if isfield(DataMat(1), 'Events') && ~isempty(DataMat(1).Events) diff --git a/toolbox/io/out_data_snirf.m b/toolbox/io/out_data_snirf.m index b4011a88cc..82442bc3f5 100644 --- a/toolbox/io/out_data_snirf.m +++ b/toolbox/io/out_data_snirf.m @@ -112,7 +112,7 @@ function out_data_snirf(ExportFile, DataMat, ChannelMatOut) % Set Measurment list -isProcessed = ~isempty(DataMat.DisplayUnits) && ( contains(DataMat.DisplayUnits, {'OD', 'mol'}) ); +isProcessed = ~isempty(DataMat.DisplayUnits); if isProcessed snirfdata.SNIRFData.data.measurementList.dataTypeLabel = ''; end @@ -188,7 +188,7 @@ function out_data_snirf(ExportFile, DataMat, ChannelMatOut) if any(evt_include) snirfdata.SNIRFData.stim = snirfdata.SNIRFData.stim(evt_include); else - snirfdata.SNIRFData = rmfield(snirfdata.SNIRFData,'stim'); + snirfdata.SNIRFData.stim = struct('name', '', 'data', zeros(0, 3)); end % Save snirf file. savesnirf(snirfdata, ExportFile); @@ -197,13 +197,14 @@ function out_data_snirf(ExportFile, DataMat, ChannelMatOut) function [dataType, dataTypeLabel] = getDataType(Channel, Unit) - + + measure_types = nst_measure_types(); [isrc, idet, chan_measures, measure_type] = nst_unformat_channels({Channel.Name}); if isempty(Unit) dataType = 1; - dataTypeLabel = ''; - elseif contains(Unit, 'OD') + dataTypeLabel = ''; + elseif measure_type == measure_types.WAVELENGTH || contains(Unit, 'OD') dataType = 99999; dataTypeLabel = 'dOD'; elseif contains(chan_measures, {'HbO', 'HbR', 'HbT'}) From c3d88bbbbcb511fa09995deef0dd9b7be6d17b2c Mon Sep 17 00:00:00 2001 From: Edouard Date: Tue, 18 Aug 2026 19:45:58 -0400 Subject: [PATCH 11/14] Revert "in_tsv return an helper struct for easier indexing to the table" This reverts commit 70b8205d86412da1b7d23774aec3bb08797b3a2c. --- toolbox/io/in_tsv.m | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/toolbox/io/in_tsv.m b/toolbox/io/in_tsv.m index 16581664c6..e04c7a9ece 100644 --- a/toolbox/io/in_tsv.m +++ b/toolbox/io/in_tsv.m @@ -1,7 +1,7 @@ -function [cTsv, ColNames, index] = in_tsv(TsvFile, ColNames, isWarning, Delimiter, isDecimalComma) +function [cTsv, ColNames] = in_tsv(TsvFile, ColNames, isWarning, Delimiter, isDecimalComma) % IN_TSV: Reads specific columns in a .tsv file % -% USAGE: [cTsv, ColNames, index] = in_tsv(TsvFile, ColNames=[all], isWarning=1, Delimiter=[tab], isDecimalComma=0) +% USAGE: [cTsv, ColNames] = in_tsv(TsvFile, ColNames=[all], isWarning=1, Delimiter=[tab], isDecimalComma=0) % % INPUTS: % - TsvFile : Full path to input filename @@ -11,12 +11,7 @@ % - isWarning : If 1, display a warning for each missing column name (only if ColNames is set) % - Delimiter : Character (eg. sprintf('\t') or ';) % - isDecimalComma : If 1, read the entire file, replace all the commas with dots, and then scan it -% OUTPULS: -% - cTsv: table value. -% - ColNames: {1 x Ncol} Cell-array of strings (eg. {'col1', 'col2'}) -% - Index: Struct, making the link between the ColNames and the columns -% of cTsv. Example: [cTsv, ColNames, index] = in_tsv(TsvFile, {'col1', -% 'col2'}): index.col1 = 1 so, cTsv can be read using: cTsv(:, index.col1) + % @============================================================================= % This function is part of the Brainstorm software: % https://neuroimage.usc.edu/brainstorm @@ -59,9 +54,7 @@ ColNames = ColNames(1,:); end % Intialize returned variable -cTsv = {}; -index = create_index(ColNames); - +cTsv = {}; % Open file fid = fopen(TsvFile, 'r'); if (fid < 0) @@ -138,12 +131,4 @@ end end end -end - -function index = create_index(ColNames) - index = struct(); - for jCol = 1:length(ColNames) - index.(ColNames{jCol}) = jCol; - end -end From 1f5cc69d0dc52b8799191d7827561e2a6b9a1ea1 Mon Sep 17 00:00:00 2001 From: Edouard Date: Tue, 18 Aug 2026 19:55:55 -0400 Subject: [PATCH 12/14] wip -- not tested --- toolbox/io/in_channel_bids_nirs.m | 56 ++++++++++++++++--------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/toolbox/io/in_channel_bids_nirs.m b/toolbox/io/in_channel_bids_nirs.m index d9c7501f6e..e395bcb903 100644 --- a/toolbox/io/in_channel_bids_nirs.m +++ b/toolbox/io/in_channel_bids_nirs.m @@ -36,15 +36,17 @@ end % Read the TSV file - [channelValue, ~, channelIndex] = in_tsv(ChannelFile, {'name', 'type', 'source', 'detector', 'wavelength_nominal', 'status', 'component'}, 0); - if isempty(channelValue) || isempty(channelValue{1, channelIndex.name}) + [channelValue, tsvCols] = in_tsv(ChannelFile, {'name', 'type', 'source', 'detector', 'wavelength_nominal', 'status', 'component'}, 0); + if isempty(channelValue) || isempty(channelValue{1, 'name'}) disp('BIDS> Error: Invalid _channels.tsv file.'); ChannelMat = []; ChannelStatus = []; return; end - + channelValue = cell2table(channelValue, 'VariableNames', tsvCols); + if ~isempty(OptodeFile) && exist(OptodeFile, 'file') - [tsvOptodes, ~, OptodesIndex] = in_tsv(OptodeFile, {'name', 'type', 'x', 'y', 'z', 'template_x', 'template_y', 'template_z'}); + [tsvOptodes, tsvCols] = in_tsv(OptodeFile, {'name', 'type', 'x', 'y', 'z', 'template_x', 'template_y', 'template_z'}); + tsvOptodes = cell2table(tsvOptodes, 'VariableNames', tsvCols); else tsvOptodes = {}; end @@ -62,16 +64,16 @@ for iChannel = 1:nChan - channel_type = upper(channelValue{iChannel, channelIndex.type}); + channel_type = upper(channelValue{iChannel, 'type'}); if any(strcmp(channel_type, {'NIRSCWAMPLITUDE', 'NIRSCWOPTICALDENSITY', 'NIRSCWHBO', 'NIRSCWHBR'})) - channel_name = parse_name(channelValue{iChannel, channelIndex.name}); + channel_name = parse_name(channelValue{iChannel, 'name'}); else - channel_name = channelValue{iChannel, channelIndex.name}; + channel_name = channelValue{iChannel, 'name'}; end switch(channel_type) case {'NIRSCWAMPLITUDE', 'NIRSCWOPTICALDENSITY'} - wl = round(str2double(channelValue{iChannel, channelIndex.wavelength_nominal})); + wl = round(str2double(channelValue{iChannel, 'wavelength_nominal'})); ChannelMat.Channel(iChannel).Name = sprintf('%sWL%d', channel_name, wl); ChannelMat.Channel(iChannel).Type = 'NIRS'; ChannelMat.Channel(iChannel).Group = sprintf('WL%d', wl); @@ -82,11 +84,11 @@ ChannelMat.Channel(iChannel).Group = sprintf('Hb%s', channel_type(end)); ChannelMat.Channel(iChannel).Weight = 1; case {'ACCEL', 'GYRO', 'MAGN'} - if isempty(channelValue{iChannel, channelIndex.component}) + if isempty(channelValue{iChannel, 'component'}) error('Componnent for channel %s is not defnied', channel_name) end - ChannelMat.Channel(iChannel).Name = sprintf('%s_%s', channel_name, channelValue{iChannel, channelIndex.component}); + ChannelMat.Channel(iChannel).Name = sprintf('%s_%s', channel_name, channelValue{iChannel, 'component'}); ChannelMat.Channel(iChannel).Type = 'Misc'; % Is there a better type ? ChannelMat.Channel(iChannel).Group = []; ChannelMat.Channel(iChannel).Weight = 1; @@ -97,17 +99,17 @@ ChannelMat.Channel(iChannel).Weight = 1; otherwise isValidChannel(iChannel) = false; - warning('Unsoprted channel %s with type %s', channelValue{iChannel, channelIndex.name}, channelValue{iChannel, channelIndex.type} ) + warning('Unsoprted channel %s with type %s', channelValue{iChannel, 'name'}, channelValue{iChannel, 'type'} ) continue; end - if ~isempty(channelValue{iChannel, channelIndex.status}) && strcmpi(channelValue{iChannel, channelIndex.status}, 'bad') + if ~isempty(channelValue{iChannel, 'status'}) && strcmpi(channelValue{iChannel, 'status'}, 'bad') ChannelStatus(iChannel) = -1; end if ~isempty(tsvOptodes) - ChannelMat.Channel(iChannel).Loc = getOptodesCoordinate(OptodesIndex, tsvOptodes, channelValue{iChannel, channelIndex.source}, channelValue{iChannel, channelIndex.detector}); + ChannelMat.Channel(iChannel).Loc = getOptodesCoordinate(tsvOptodes, channelValue{iChannel, 'source'}, channelValue{iChannel, 'detector'}); end end @@ -157,7 +159,7 @@ channel_name = strrep(channel_name, 'Rx','D'); end -function coordinates = getOptodesCoordinate(OptodesIndex, tsvOptodes, sourceName, detectorName) +function coordinates = getOptodesCoordinate(tsvOptodes, sourceName, detectorName) coordinates = []; if strcmp(detectorName, 'n/a') || strcmp(sourceName, 'n/a') @@ -165,37 +167,37 @@ end % Read optodes coordinate - iSource = find(strcmp(tsvOptodes(:, OptodesIndex.name), sourceName)); + iSource = find(strcmp(tsvOptodes(:, 'name'), sourceName)); if isempty(iSource) warning('Unable to find source %s in optodes.tsv', sourceName) return; - elseif ~strcmp(tsvOptodes{iSource, OptodesIndex.type}, 'source') + elseif ~strcmp(tsvOptodes{iSource, 'type'}, 'source') warning('%s should be a source but is labelled as a %s in optodes.tsv', sourceName, tsvOptodes{iSource,2}); return; end - if ~isempty(tsvOptodes{iSource, OptodesIndex.x}) && ~isempty(tsvOptodes{iSource, OptodesIndex.y}) && ~isempty(tsvOptodes{iSource,OptodesIndex.z}) - source_coord = [str2double(tsvOptodes{iSource, OptodesIndex.x}); str2double(tsvOptodes{iSource, OptodesIndex.y}); str2double(tsvOptodes{iSource, OptodesIndex.z})]; - elseif ~isempty(tsvOptodes{iSource, OptodesIndex.template_x}) && ~isempty(tsvOptodes{iSource, OptodesIndex.template_y}) && ~isempty(tsvOptodes{iSource, OptodesIndex.template_z}) - source_coord = [str2double(tsvOptodes{iSource, OptodesIndex.template_x}); str2double(tsvOptodes{iSource, OptodesIndex.template_y}); str2double(tsvOptodes{iSource, OptodesIndex.template_z})]; + if ~isempty(tsvOptodes{iSource, 'x'}) && ~isempty(tsvOptodes{iSource, 'y'}) && ~isempty(tsvOptodes{iSource, 'z'}) + source_coord = [str2double(tsvOptodes{iSource, 'x'}); str2double(tsvOptodes{iSource, 'y'}); str2double(tsvOptodes{iSource, 'z'})]; + elseif ~isempty(tsvOptodes{iSource, 'template_x'}) && ~isempty(tsvOptodes{iSource, 'template_y'}) && ~isempty(tsvOptodes{iSource, 'template_z'}) + source_coord = [str2double(tsvOptodes{iSource, 'template_x'}); str2double(tsvOptodes{iSource, 'template_y'}); str2double(tsvOptodes{iSource, 'template_z'})]; else warning('No coordinate available for %s in optodes.tsv', sourceName) return; end - iDetector = find(strcmp(tsvOptodes(:, OptodesIndex.name), detectorName)); + iDetector = find(strcmp(tsvOptodes(:, 'name'), detectorName)); if isempty(iDetector) warning('Unable to find detector %s in optodes.tsv', detectorName) return; - elseif ~strcmp(tsvOptodes{iDetector, OptodesIndex.type}, 'detector') - warning('%s should be a detector but is labelled as a %s in optodes.tsv', detectorName, tsvOptodes{iSource, OptodesIndex.type}); + elseif ~strcmp(tsvOptodes{iDetector, 'type'}, 'detector') + warning('%s should be a detector but is labelled as a %s in optodes.tsv', detectorName, tsvOptodes{iSource, 'type'}); return; end - if ~isempty(tsvOptodes{iDetector, OptodesIndex.x}) && ~isempty(tsvOptodes{iDetector, OptodesIndex.y}) && ~isempty(tsvOptodes{iDetector, OptodesIndex.z}) - detector_coord = [str2double(tsvOptodes{iDetector, OptodesIndex.x}) ; str2double(tsvOptodes{iDetector, OptodesIndex.y}); str2double(tsvOptodes{iDetector, OptodesIndex.z})]; - elseif ~isempty(tsvOptodes{iDetector,OptodesIndex.template_x}) && ~isempty(tsvOptodes{iDetector, OptodesIndex.template_y}) && ~isempty(tsvOptodes{iDetector, OptodesIndex.template_z}) - detector_coord = [str2double(tsvOptodes{iDetector, OptodesIndex.template_x}) ; str2double(tsvOptodes{iDetector, OptodesIndex.template_y}); str2double(tsvOptodes{iDetector, OptodesIndex.template_z})]; + if ~isempty(tsvOptodes{iDetector, 'x'}) && ~isempty(tsvOptodes{iDetector, 'y'}) && ~isempty(tsvOptodes{iDetector, 'z'}) + detector_coord = [str2double(tsvOptodes{iDetector, 'x'}) ; str2double(tsvOptodes{iDetector, 'y'}); str2double(tsvOptodes{iDetector, 'z'})]; + elseif ~isempty(tsvOptodes{iDetector, 'template_x'}) && ~isempty(tsvOptodes{iDetector, 'template_y'}) && ~isempty(tsvOptodes{iDetector, 'template_z'}) + detector_coord = [str2double(tsvOptodes{iDetector, 'template_x'}) ; str2double(tsvOptodes{iDetector, 'template_y'}); str2double(tsvOptodes{iDetector, 'template_z'})]; else warning('No coordinate available for %s in optodes.tsv', detectorName) return; From a43aeccf41ccddc9d8622d64831d5a62b4899398 Mon Sep 17 00:00:00 2001 From: Edouard Date: Tue, 18 Aug 2026 19:56:31 -0400 Subject: [PATCH 13/14] fix NaN --- toolbox/io/in_fopen_bstmat.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolbox/io/in_fopen_bstmat.m b/toolbox/io/in_fopen_bstmat.m index 52eebf5819..b0e655decd 100644 --- a/toolbox/io/in_fopen_bstmat.m +++ b/toolbox/io/in_fopen_bstmat.m @@ -55,7 +55,7 @@ if length(DataMat(1).Time) >= 2 sFile.prop.sfreq = 1 ./ (DataMat(1).Time(2) - DataMat(1).Time(1)); else - sFile.prop.sfreq = nan; + sFile.prop.sfreq = NaN; end sFile.prop.currCtfComp = 3; sFile.prop.destCtfComp = 3; From 134747c803c0ab2911047622c5dea2e4464abed7 Mon Sep 17 00:00:00 2001 From: Edouard Date: Wed, 19 Aug 2026 12:54:56 -0400 Subject: [PATCH 14/14] bugfix --- toolbox/io/in_channel_bids_nirs.m | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/toolbox/io/in_channel_bids_nirs.m b/toolbox/io/in_channel_bids_nirs.m index e395bcb903..7eea605d43 100644 --- a/toolbox/io/in_channel_bids_nirs.m +++ b/toolbox/io/in_channel_bids_nirs.m @@ -37,7 +37,7 @@ % Read the TSV file [channelValue, tsvCols] = in_tsv(ChannelFile, {'name', 'type', 'source', 'detector', 'wavelength_nominal', 'status', 'component'}, 0); - if isempty(channelValue) || isempty(channelValue{1, 'name'}) + if isempty(channelValue) disp('BIDS> Error: Invalid _channels.tsv file.'); ChannelMat = []; ChannelStatus = []; return; @@ -64,11 +64,11 @@ for iChannel = 1:nChan - channel_type = upper(channelValue{iChannel, 'type'}); + channel_type = upper(char(channelValue{iChannel, 'type'})); if any(strcmp(channel_type, {'NIRSCWAMPLITUDE', 'NIRSCWOPTICALDENSITY', 'NIRSCWHBO', 'NIRSCWHBR'})) - channel_name = parse_name(channelValue{iChannel, 'name'}); + channel_name = parse_name(char(channelValue{iChannel, 'name'})); else - channel_name = channelValue{iChannel, 'name'}; + channel_name = char(channelValue{iChannel, 'name'}); end switch(channel_type) @@ -109,7 +109,7 @@ if ~isempty(tsvOptodes) - ChannelMat.Channel(iChannel).Loc = getOptodesCoordinate(tsvOptodes, channelValue{iChannel, 'source'}, channelValue{iChannel, 'detector'}); + ChannelMat.Channel(iChannel).Loc = getOptodesCoordinate(tsvOptodes, char(channelValue{iChannel, 'source'}), char(channelValue{iChannel, 'detector'})); end end @@ -167,7 +167,7 @@ end % Read optodes coordinate - iSource = find(strcmp(tsvOptodes(:, 'name'), sourceName)); + iSource = find(strcmp(tsvOptodes{:, 'name'},sourceName)); if isempty(iSource) warning('Unable to find source %s in optodes.tsv', sourceName) return; @@ -185,7 +185,7 @@ return; end - iDetector = find(strcmp(tsvOptodes(:, 'name'), detectorName)); + iDetector = find(strcmp(tsvOptodes{:, 'name'}, detectorName)); if isempty(iDetector) warning('Unable to find detector %s in optodes.tsv', detectorName) return;