From d08e1e6b180470b9159115308b452ed44bc73196 Mon Sep 17 00:00:00 2001 From: Eshref Yozdemir Date: Mon, 13 Apr 2020 11:01:58 +0200 Subject: [PATCH 01/31] Merge pull request #102 from bachlab/feature/separate-blink-saccade-preprocessing Factor out blink saccade preprocessing --- src/backroom/blink_saccade_filtering.m | 35 ++++++ src/backroom/set_blinks_saccades_to_nan.m | 11 +- src/pspm_blink_saccade_filt.m | 128 ++++++++++++++++++++++ src/pspm_get_eyelink.m | 40 ++----- test/pspm_blink_saccade_filt_test.m | 71 ++++++++++++ test/pspm_test.m | 19 +++- 6 files changed, 267 insertions(+), 37 deletions(-) create mode 100644 src/backroom/blink_saccade_filtering.m create mode 100644 src/pspm_blink_saccade_filt.m create mode 100644 test/pspm_blink_saccade_filt_test.m diff --git a/src/backroom/blink_saccade_filtering.m b/src/backroom/blink_saccade_filtering.m new file mode 100644 index 000000000..200c578f6 --- /dev/null +++ b/src/backroom/blink_saccade_filtering.m @@ -0,0 +1,35 @@ +function [out_data_mat] = blink_saccade_filtering(data_mat, column_names, mask_chans, n_samples, fn_is_left) + if nargin == 4 + fn_is_left = @(x) endsWith(x, '_l'); + end + + out_data_mat = expand_mask_chans(data_mat, column_names, mask_chans, n_samples); + out_data_mat = set_blinks_saccades_to_nan(out_data_mat, column_names, mask_chans); +end + +function data = expand_mask_chans(data, column_names, mask_chans, offset) + for chan = mask_chans + col_idx = find(strcmpi(column_names, chan{1})); + data(:, col_idx) = expand_mask(data(:, col_idx), offset); + end +end + +function mask = expand_mask(mask, offset) + diffmask = diff(mask); + indices_to_expand_towards_left = find(diffmask == 1) + 1; + indices_to_expand_towards_right = find(diffmask == (-1)); + + for ii = 1:numel(indices_to_expand_towards_left) + idx = indices_to_expand_towards_left(ii); + begidx = max(1, idx - offset); + endidx = max(1, idx - 1); + mask(begidx : endidx) = true; + end + ndata = numel(mask); + for ii = 1:numel(indices_to_expand_towards_right) + idx = indices_to_expand_towards_right(ii); + begidx = min(ndata, idx + 1); + endidx = min(ndata, idx + offset); + mask(begidx : endidx) = true; + end +end diff --git a/src/backroom/set_blinks_saccades_to_nan.m b/src/backroom/set_blinks_saccades_to_nan.m index 567c47ef6..a1d01fe7e 100644 --- a/src/backroom/set_blinks_saccades_to_nan.m +++ b/src/backroom/set_blinks_saccades_to_nan.m @@ -11,8 +11,12 @@ % gaze_x_r, etc. % column_names: Name of each column in the input data_mat. % mask_chans: Names of blink and saccade channels. + % + % Optional Inputs + % --------------- % fn_is_left: Function that takes a LOWERCASED channel name as input and returns true if the channel - % name belongs to left eye. Otherwise, it returns false. + % name belongs to left eye. Otherwise, it returns false. By default, this function checks + % if the channel name ends with '_l'. % % Output % ------ @@ -23,7 +27,10 @@ % % - all elements in right data columns (except blink/saccade) that correspond to right % blink or saccade rows are set to NaN - % + if nargin == 3 + fn_is_left = @(x) endsWith(x, '_l'); + end + column_names = cellfun(@(x) lower(x), column_names, 'uni', 0); mask_chans = cellfun(@(x) lower(x), mask_chans, 'uni', 0); diff --git a/src/pspm_blink_saccade_filt.m b/src/pspm_blink_saccade_filt.m new file mode 100644 index 000000000..eb121a96b --- /dev/null +++ b/src/pspm_blink_saccade_filt.m @@ -0,0 +1,128 @@ +function [sts, out_channel] = pspm_blink_saccade_filt(fn, discard_factor, options) + % Perform blink-saccade filtering on a given file containing pupil data. This + % function extends each blink and/or saccade period towards the beginning and the + % end of the signal by an amount specified by the user. + % + % FORMAT: [sts, out_channel] = pspm_blink_saccade_filt(fn, discard_factor, options) + % + % fn: [string] Path to the PsPM file which contains + % the pupil data. + % + % discard_factor: [numeric] Factor used to determine the number of + % samples right before and right after a blink/saccade + % period to discard. This value is multiplied by the + % sampling rate of the recording to determine the + % number of samples to discard from one end. Therefore, + % for each blink/saccade period, 2*this_value*SR many + % samples are discarded in total, and effectively + % blink/saccade period is extended. + % + % This value also corresponds to the duration of + % samples to discard on one end in seconds. For example, + % when it is 0.01, we discard 10 ms worth of data on + % each end of every blink/saccade period. + % options: + % Optional: + % channel: [numeric/string] Channel ID to be preprocessed. + % By default preprocesses all the pupil and gaze + % channels. + % (Default: 0) + % + % channel_action: ['add'/'replace'] Defines whether corrected data + % should be added or the corresponding preprocessed + % channel should be replaced. + % (Default: 'add') + % + % + %__________________________________________________________________________ + % (C) 2020 Eshref Yozdemir (University of Zurich) + + global settings; + if isempty(settings), pspm_init; end + sts = -1; + + if nargin == 2 + options = struct(); + end + if ~isfield(options, 'channel') + options.channel = 0; + end + if ~isfield(options, 'channel_action') + options.channel_action = 'add'; + end + + if ~isnumeric(discard_factor) + warning('ID:invalid_input', 'discard_factor must be numeric'); + return; + end + if ~ismember(options.channel_action, {'add', 'replace'}) + warning('ID:invalid_input', 'Option channel_action must be either ''add'' or ''replace'''); + return; + end + + % READ DATA + % --------- + [lsts, ~, data] = pspm_load_data(fn); + if lsts ~= 1; return; end; + [lsts, ~, data_user] = pspm_load_data(fn, options.channel); + if lsts ~= 1; return; end; + data_user = keep_pupil_gaze_chans(data_user); + + % BUILD MATRICES AND LISTS + % ------------------------ + data_mat = {}; + column_names = {}; + mask_chans = {}; + for i = 1:numel(data) + chantype = data{i}.header.chantype; + if startsWith(chantype, 'blink') || startsWith(chantype, 'saccade') + mask_chans{end + 1} = chantype; + data_mat{end + 1} = data{i}.data; + column_names{end + 1} = chantype; + end + end + n_mask_chans = numel(data_mat); + for i = 1:numel(data_user) + chantype = data_user{i}.header.chantype; + should_add = options.channel ~= 0 || startsWith(chantype, 'pupil') || startsWith(chantype, 'gaze'); + if should_add + data_mat{end + 1} = data_user{i}.data; + column_names{end + 1} = data_user{i}.header.chantype; + end + end + data_mat = cell2mat(data_mat); + + % PERFORM FILTERING + % ----------------- + addpath(pspm_path('backroom')); + sr = data{1}.header.sr; + samples_to_discard = round(sr * discard_factor); + out_mat = blink_saccade_filtering(data_mat, column_names, mask_chans, samples_to_discard); + rmpath(pspm_path('backroom')); + + % WRITE BACK + % ---------- + for i = 1:numel(data_user) + data_user{i}.data = out_mat(:, n_mask_chans + i); + end + channel_str = options.channel; + if isnumeric(channel_str) + channel_str = num2str(channel_str); + end + o.msg.prefix = sprintf('Blink saccade filtering :: Input channel: %s', channel_str); + [lsts, out_id] = pspm_write_channel(fn, data_user, options.channel_action, o); + if lsts ~= 1; return; end; + + out_channel = out_id.channel; + sts = 1; +end + +function [out_cell] = keep_pupil_gaze_chans(in_cell) + out_cell = {}; + for i = 1:numel(in_cell) + channel = lower(in_cell{i}.header.chantype); + if contains(channel, 'pupil') || contains(channel, 'gaze') + out_cell{end + 1} = in_cell{i}; + end + end +end diff --git a/src/pspm_get_eyelink.m b/src/pspm_get_eyelink.m index 329671989..a3c1e37dc 100644 --- a/src/pspm_get_eyelink.m +++ b/src/pspm_get_eyelink.m @@ -25,6 +25,12 @@ % in which eyelink_trackdist is given % % .blink_saccade_edge_discard_factor: + % DEPRECATED: This option is deprecated, and will be + % removed in the next API breaking PsPM release. Please + % change your codes to use the new + % pspm_blink_saccade_filt function if you rely on this + % functionality. + % % Factor used to determine the number of % samples right before and right after a blink/saccade % period to discard. This value is multiplied by the @@ -70,6 +76,10 @@ for i = 1:numel(import) if ~isfield(import{i}, 'blink_saccade_edge_discard_factor') import{i}.blink_saccade_edge_discard_factor = default_blink_saccade_discard_factor; + else + warning('ID:deprecated', ['pspm_get_eyelink: blink_saccade_edge_discard_factor argument is DEPRECATED.'... + 'Please change your code to use the new pspm_blink_saccade_filt function if you rely on this'... + 'functionality.']); end if ~isnumeric(import{i}.blink_saccade_edge_discard_factor) || ... @@ -108,13 +118,12 @@ else expand_factor = default_blink_saccade_discard_factor; end - data{i}.channels = expand_mask_chans(... + data{i}.channels = blink_saccade_filtering(... data{i}.channels, ... data{i}.channels_header, ... mask_chans, ... expand_factor * data{i}.sampleRate ... ); - data{i}.channels = set_blinks_saccades_to_nan(data{i}.channels, data{i}.channels_header, mask_chans, @(x) endsWith(x, '_l')); end rmpath(pspm_path('backroom')); @@ -409,30 +418,3 @@ sts = 1; return; end - -function data = expand_mask_chans(data, column_names, mask_chans, offset) - for chan = mask_chans - col_idx = find(strcmpi(column_names, chan{1})); - data(:, col_idx) = expand_mask(data(:, col_idx), offset); - end -end - -function mask = expand_mask(mask, offset) - diffmask = diff(mask); - indices_to_expand_towards_left = find(diffmask == 1) + 1; - indices_to_expand_towards_right = find(diffmask == (-1)); - - for ii = 1:numel(indices_to_expand_towards_left) - idx = indices_to_expand_towards_left(ii); - begidx = max(1, idx - offset); - endidx = max(1, idx - 1); - mask(begidx : endidx) = true; - end - ndata = numel(mask); - for ii = 1:numel(indices_to_expand_towards_right) - idx = indices_to_expand_towards_right(ii); - begidx = min(ndata, idx + 1); - endidx = min(ndata, idx + offset); - mask(begidx : endidx) = true; - end -end diff --git a/test/pspm_blink_saccade_filt_test.m b/test/pspm_blink_saccade_filt_test.m new file mode 100644 index 000000000..3c9531972 --- /dev/null +++ b/test/pspm_blink_saccade_filt_test.m @@ -0,0 +1,71 @@ +classdef pspm_blink_saccade_filt_test < pspm_get_superclass +% PSPM_BLINK_SACCADE_FILT_TEST +% unittest class for the pspm_blink_saccade_filt function +%__________________________________________________________________________ +% (C) 2019 Eshref Yozdemir (University of Zurich) + + properties + fn = fullfile('ImportTestData', 'eyelink', 'u_sc4b31.asc'); + testcases; + fhandle = @pspm_blink_saccade_filt; + end + + methods + function define_testcases(this) + + end + end + + methods (Test) + function invalid_input(this) + this.verifyWarning(@()pspm_blink_saccade_filt(this.fn, 'str'), 'ID:invalid_input'); + options.channel_action = 'delete'; + this.verifyWarning(@()pspm_blink_saccade_filt(this.fn, 0, options), 'ID:invalid_input'); + end + + function test_filtering(this) + factor_list = [0, 0.001, 0.01, 0.1, 1]; + for discard_factor = factor_list + import{1}.type = 'pupil_r'; + import{1}.eyelink_trackdist = 700; + import{1}.distance_unit = 'mm'; + import{2}.type = 'gaze_x_r'; + import{3}.type = 'gaze_y_r'; + import{4}.type = 'blink_r' + import{5}.type = 'saccade_r'; + options.eyelink_trackdist = 700; + options.distance_unit = 'mm'; + options.overwrite = true; + + fn_imported = pspm_import(this.fn, 'eyelink', import, options); + fn_imported = fn_imported{1}; + [sts, ~, data_old] = pspm_load_data(fn_imported); + + options = struct('channel_action', 'replace'); + pspm_blink_saccade_filt(fn_imported, discard_factor, options); + [sts, ~, data_new] = pspm_load_data(fn_imported); + + N = numel(data_old{1}.data); + n_remove = round(discard_factor * data_old{1}.header.sr); + blink_r_indices = find(data_old{4}.data); + sacc_r_indices = find(data_old{5}.data); + for i = 1:3 + this.verifyTrue(assert_nan(data_new{i}.data, blink_r_indices, N, n_remove)); + this.verifyTrue(assert_nan(data_new{i}.data, sacc_r_indices, N, n_remove)); + end + end + end + end +end + +function out = assert_nan(data, indices, N, n_remove) + for idx = indices + lo = max(1, idx - n_remove); + hi = min(N, idx + n_remove); + if ~all(isnan(data(lo:hi))) + out = false; + return; + end + end + out = true; +end diff --git a/test/pspm_test.m b/test/pspm_test.m index abbe67869..d09bc9d27 100644 --- a/test/pspm_test.m +++ b/test/pspm_test.m @@ -23,7 +23,8 @@ function pspm_test(varargin) % build suits % ------------------------------------------------------------------------- - suite = [ TestSuite.fromClass(?pspm_load_data_test), ... + suite = [... + TestSuite.fromClass(?pspm_load_data_test), ... TestSuite.fromClass(?pspm_write_channel_test), ... TestSuite.fromClass(?pspm_trim_test), ... TestSuite.fromClass(?pspm_find_channel_test), ... @@ -51,10 +52,13 @@ function pspm_test(varargin) TestSuite.fromClass(?pspm_pupil_correct_eyelink_test), ... TestSuite.fromClass(?pspm_pupil_correct_test), ... TestSuite.fromClass(?pspm_pupil_pp_test), ... - TestSuite.fromClass(?set_blinks_saccades_to_nan_test)]; + TestSuite.fromClass(?set_blinks_saccades_to_nan_test), ... + TestSuite.fromClass(?pspm_blink_saccade_filt_test), ... + ]; - import_suite = [ TestSuite.fromClass(?pspm_get_acq_test), ... + import_suite = [... + TestSuite.fromClass(?pspm_get_acq_test), ... TestSuite.fromClass(?pspm_get_acqmat_test), ... TestSuite.fromClass(?pspm_get_acq_bioread_test), ... TestSuite.fromClass(?pspm_get_biograph_test), ... @@ -76,16 +80,19 @@ function pspm_test(varargin) TestSuite.fromClass(?pspm_get_smi_test), ... TestSuite.fromClass(?import_eyelink_test), ... TestSuite.fromClass(?import_viewpoint_test), ... - TestSuite.fromClass(?import_smi_test)]; + TestSuite.fromClass(?import_smi_test), ... + ]; - chantype_suite = [ TestSuite.fromClass(?pspm_get_ecg_test), ... + chantype_suite = [... + TestSuite.fromClass(?pspm_get_ecg_test), ... TestSuite.fromClass(?pspm_get_events_test), ... TestSuite.fromClass(?pspm_get_hb_test), ... TestSuite.fromClass(?pspm_get_hr_test), ... TestSuite.fromClass(?pspm_get_marker_test), ... TestSuite.fromClass(?pspm_get_pupil_test), ... TestSuite.fromClass(?pspm_get_resp_test), ... - TestSuite.fromClass(?pspm_get_scr_test)]; + TestSuite.fromClass(?pspm_get_scr_test), ... + ]; full_suite = [suite, import_suite, chantype_suite]; From 1aef049af55e3a1383a5450ac4c326f071dedf24 Mon Sep 17 00:00:00 2001 From: Eshref Yozdemir Date: Wed, 15 Apr 2020 11:58:52 +0200 Subject: [PATCH 02/31] Merge pull request #106 from bachlab/bugfix/pupil-correct-last-gaze-chan Use the last gaze chan in pupil_correct_eyelink --- src/pspm_pupil_correct_eyelink.m | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/pspm_pupil_correct_eyelink.m b/src/pspm_pupil_correct_eyelink.m index 92df8d691..79b75a13c 100644 --- a/src/pspm_pupil_correct_eyelink.m +++ b/src/pspm_pupil_correct_eyelink.m @@ -217,6 +217,15 @@ [lsts, infos, gaze_y_data] = pspm_load_data(fn, gaze_y_chan); if lsts ~= 1; return; end + if numel(gaze_x_data) > 1 + warning('ID:multiple_channels', 'There are more than one gaze x channel. We will use the last one'); + gaze_x_data = gaze_x_data(end:end); + end + if numel(gaze_y_data) > 1 + warning('ID:multiple_channels', 'There are more than one gaze y channel. We will use the last one'); + gaze_y_data = gaze_y_data(end:end); + end + % conditionally mandatory input checks % ------------------------------------------------------------------------- if strcmp(gaze_x_data{1}.header.units, 'pixel') || strcmp(gaze_y_data{1}.header.units, 'pixel') From 777d92bc51f10ab25b9c24913fd09cff6f997183 Mon Sep 17 00:00:00 2001 From: irojkov-ph <56234933+irojkov-ph@users.noreply.github.com> Date: Mon, 4 May 2020 09:27:48 +0200 Subject: [PATCH 03/31] Merge pull request #108 from bachlab/bugfix/extract-segments-nan-perc Mult NaN ratio with 100 in extract_segments --- src/pspm_extract_segments.m | 6 +++--- test/pspm_extract_segments_test.m | 8 ++++---- test/pspm_glm_test.m | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pspm_extract_segments.m b/src/pspm_extract_segments.m index ce3d639a3..2b1497abf 100644 --- a/src/pspm_extract_segments.m +++ b/src/pspm_extract_segments.m @@ -613,8 +613,8 @@ segments{c}.std = nanstd(m,0,2); segments{c}.sem = segments{c}.std./sqrt(n_onsets_in_cond{c}); - segments{c}.trial_nan_percent = sum(isnan(m))/size(m,1); - segments{c}.total_nan_percent = sum(sum(isnan(m)))/numel(m); + segments{c}.trial_nan_percent = 100.0 * sum(isnan(m))/size(m,1); + segments{c}.total_nan_percent = 100.0 * sum(sum(isnan(m)))/numel(m); % segments{c}.total_nan_percent = mean(segments{c}.trial_nan_percent); @@ -729,4 +729,4 @@ end; sts = 1; - \ No newline at end of file + diff --git a/test/pspm_extract_segments_test.m b/test/pspm_extract_segments_test.m index ad8feeb6c..4bc82c5f1 100644 --- a/test/pspm_extract_segments_test.m +++ b/test/pspm_extract_segments_test.m @@ -147,8 +147,8 @@ control_data{i}.cond_name = cond_names{i}; control_data{i}.mean = cond_trial_mean{i}; control_data{i}.std = cond_trial_std{i}; - control_data{i}.trial_nan_percent = cond_nan_trial{i}; - control_data{i}.total_nan_percent = cond_nan_total{i}; + control_data{i}.trial_nan_percent = cond_nan_trial{i}*100; + control_data{i}.total_nan_percent = cond_nan_total{i}*100; end end end @@ -217,7 +217,7 @@ function test_manual_length(this,nr_trial,nan_ratio) this.verifyEqual(seg.name,control.cond_name); this.verifyEqual(seg.mean,control.mean); this.verifyEqual(seg.std,control.std); - this.verifyEqual(seg.trial_nan_percent,control.trial_nan_percent); + this.verifyTrue(all(abs(seg.trial_nan_percent-control.trial_nan_percent)<1e-12)); this.verifyTrue(abs(seg.total_nan_percent-control.total_nan_percent)<1e-12); end end @@ -247,7 +247,7 @@ function test_manual_duration(this,nr_trial,nan_ratio) this.verifyEqual(seg.name,control.cond_name); this.verifyEqual(seg.mean,control.mean); this.verifyEqual(seg.std,control.std); - this.verifyEqual(seg.trial_nan_percent,control.trial_nan_percent); + this.verifyTrue(all(abs(seg.trial_nan_percent-control.trial_nan_percent)<1e-12)); this.verifyTrue(abs(seg.total_nan_percent-control.total_nan_percent)<1e-12); end end diff --git a/test/pspm_glm_test.m b/test/pspm_glm_test.m index 7a8aea7e1..582ff3300 100644 --- a/test/pspm_glm_test.m +++ b/test/pspm_glm_test.m @@ -650,10 +650,10 @@ function invalid_input(this) nr_nan_toadd = round(nan_percent * nr_samples); idx_replace = randsample(nr_samples, nr_nan_toadd); Y(idx_replace) = NaN; - new_nan_percent = sum(isnan(Y))/nr_samples; + new_nan_percent = sum(isnan(Y))/nr_samples * 100; else - new_nan_percent = nan_percent; + new_nan_percent = nan_percent * 100; end %t @@ -666,7 +666,7 @@ function invalid_input(this) this.verifyEqual(length(glm.stats_missing),exptected_number_of_conditions, sprintf('test_extract_missing: glm.stats_missing does not have the expected number (%i) of elements', exptected_number_of_conditions)); this.verifyEqual(length(glm.stats_exclude),exptected_number_of_conditions, sprintf('test_extract_missing: glm.stats_exclude does not have the expected number (%i) of elements', exptected_number_of_conditions)); - this.verifyTrue((abs(mean(glm.stats_missing)-new_nan_percent)<0.01), sprintf('test_extract_missing: mean of glm.stats_missing (%i) does not correspond to expected nan_percentage (%i)', mean(glm.stats_missing), new_nan_percent)); + this.verifyTrue((abs(mean(glm.stats_missing)-new_nan_percent) < 1), sprintf('test_extract_missing: mean of glm.stats_missing (%i) does not correspond to expected nan_percentage (%i)', mean(glm.stats_missing), new_nan_percent)); check_values = glm.stats_missing > cutoff; this.verifyTrue(all(glm.stats_exclude == check_values), sprintf('test_extract_missing: glm.stats_exclude does not exclude the right conditions')); From b98c3c2625a23433cec6277977c9c855d4894768 Mon Sep 17 00:00:00 2001 From: Sam Maxwell Date: Wed, 17 Jun 2020 11:58:03 +0100 Subject: [PATCH 04/31] Merge pull request #121 from bachlab/fix/load-variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(butter): assign filt to variable from load as required by Matlab … --- src/pspm_butter.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pspm_butter.m b/src/pspm_butter.m index a3472ff6f..3b4111a4b 100644 --- a/src/pspm_butter.m +++ b/src/pspm_butter.m @@ -38,7 +38,7 @@ if settings.signal [b, a]=butter(order, freqratio, pass); else - load('pspm_butter.mat'); + filt = load('pspm_butter.mat').filt; switch pass case 'low' f = filt{1}; From 81b0237b364ddcd795ce08f395c3912744021af7 Mon Sep 17 00:00:00 2001 From: irojkov-ph <56234933+irojkov-ph@users.noreply.github.com> Date: Mon, 29 Jun 2020 14:02:27 +0200 Subject: [PATCH 05/31] Merge pull request #125 from bachlab/fix/load-struct fix(struct-load): access after load --- src/pspm_butter.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pspm_butter.m b/src/pspm_butter.m index 3b4111a4b..ad5cbc734 100644 --- a/src/pspm_butter.m +++ b/src/pspm_butter.m @@ -38,12 +38,12 @@ if settings.signal [b, a]=butter(order, freqratio, pass); else - filt = load('pspm_butter.mat').filt; + F = load('pspm_butter.mat', 'filt'); switch pass case 'low' - f = filt{1}; + f = F.filt{1}; case 'high' - f = filt{2}; + f = F.filt{2}; end; d = abs([f.freqratio] - freqratio); n = find(d < .0001); From fe26723c81dcb3c3d34ca24385e99c6a9aec3b92 Mon Sep 17 00:00:00 2001 From: irojkov-ph <56234933+irojkov-ph@users.noreply.github.com> Date: Mon, 29 Jun 2020 14:34:47 +0200 Subject: [PATCH 06/31] Merge pull request #126 from bachlab/fix/rev-dcm-x-tick fix(dcm-xticks): scale xtick label by sample rate --- src/pspm_rev_dcm.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pspm_rev_dcm.m b/src/pspm_rev_dcm.m index 16efb824c..9a682d7e7 100644 --- a/src/pspm_rev_dcm.m +++ b/src/pspm_rev_dcm.m @@ -78,6 +78,8 @@ function pspm_rev_dcm(dcm, job, sn, trl) data = [y(win), yhat(win)]; subplot(f.r, f.c, n); plot(data); + xt = get(gca, 'XTick'); + set(gca, 'XTickLabel', xt * dcm.input.sr); set(gca, 'YLim', [min(yhat), max(yhat)]); end; From 42786eadae42356b6adc2660a281f7e562f35fcb Mon Sep 17 00:00:00 2001 From: Dominik Bach Date: Mon, 29 Jun 2020 17:22:23 +0100 Subject: [PATCH 07/31] Merge pull request #122 from bachlab/feat/missing-epochs-file-out Feat/missing epochs file out --- src/pspm_cfg/pspm_cfg_artefact_rm.m | 22 +++++- src/pspm_cfg/pspm_cfg_run_artefact_rm.m | 3 + src/pspm_data_editor.fig | Bin 53758 -> 71101 bytes src/pspm_data_editor.m | 90 ++++++++++++++++++++++-- src/pspm_pp.m | 20 +++--- src/pspm_simple_qa.m | 46 +++++++++--- test/pspm_pp_test.m | 43 +++++++++++ 7 files changed, 200 insertions(+), 24 deletions(-) diff --git a/src/pspm_cfg/pspm_cfg_artefact_rm.m b/src/pspm_cfg/pspm_cfg_artefact_rm.m index b5fee3886..c5cfb7619 100644 --- a/src/pspm_cfg/pspm_cfg_artefact_rm.m +++ b/src/pspm_cfg/pspm_cfg_artefact_rm.m @@ -69,10 +69,30 @@ qa_slope.val = {10}; qa_slope.help = {'Maximum SCR slope in microsiemens per second.'}; +qa_missing_epochs_no_filename = cfg_const; +qa_missing_epochs_no_filename.name = 'Do not write to file'; +qa_missing_epochs_no_filename.tag = 'no_missing_epochs'; +qa_missing_epochs_no_filename.val = {0}; +qa_missing_epochs_no_filename.help = {'Do not store artefacts epochs to file'}; + +qa_missing_epochs_filename_path = cfg_entry; +qa_missing_epochs_filename_path.name = 'Write to filename'; +qa_missing_epochs_filename_path.tag = 'missing_epochs_filename_path'; +qa_missing_epochs_filename_path.strtype = 's'; +qa_missing_epochs_filename_path.num = [ 1 Inf ]; +qa_missing_epochs_filename_path.help = {'Filename to store artefact epochs. Provide only the name and not extension, the file will be stored as a .mat file'}; + +qa_missing_epochs_filename = cfg_choice; +qa_missing_epochs_filename.name = 'Missing epochs file'; +qa_missing_epochs_filename.tag = 'missing_epochs'; +qa_missing_epochs_filename.val = {qa_missing_epochs_no_filename}; +qa_missing_epochs_filename.values = {qa_missing_epochs_no_filename, qa_missing_epochs_filename_path}; +qa_missing_epochs_filename.help = {'Artefact epochs file behaviour'}; + qa = cfg_branch; qa.name = 'Simple SCR quality correction'; qa.tag = 'simple_qa'; -qa.val = {qa_min, qa_max, qa_slope}; +qa.val = {qa_min, qa_max, qa_slope, qa_missing_epochs_filename}; qa.help = {['Simple SCR quality correction. See I. R. Kleckner et al.,"Simple, Transparent, and' ... 'Flexible Automated Quality Assessment Procedures for Ambulatory Electrodermal Activity Data," in ' ... 'IEEE Transactions on Biomedical Engineering, vol. 65, no. 7, pp. 1460-1467, July 2018.']}; diff --git a/src/pspm_cfg/pspm_cfg_run_artefact_rm.m b/src/pspm_cfg/pspm_cfg_run_artefact_rm.m index 01371526d..b7fcf7064 100644 --- a/src/pspm_cfg/pspm_cfg_run_artefact_rm.m +++ b/src/pspm_cfg/pspm_cfg_run_artefact_rm.m @@ -22,6 +22,9 @@ out = pspm_pp(filtertype, datafile, freq, channelnumber, options); case 'simple_qa' qa = job.filtertype.(filtertype); + if isfield(qa.missing_epochs, 'missing_epochs_filename_path') + qa.missing_epochs_filename = qa.missing_epochs.missing_epochs_filename_path; + end out = pspm_pp(filtertype, datafile, qa, channelnumber, options); end diff --git a/src/pspm_data_editor.fig b/src/pspm_data_editor.fig index 10f501d64a73c451ad7c20b07a17ec0e79e2cdcf..038262e5cb1e7f7fcc1c95ee209acbd46d3d98a2 100644 GIT binary patch literal 71101 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�qFht7T|&4 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 z�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 0 if ischar(varargin{1}) if exist(varargin{1}, 'file') @@ -122,12 +132,21 @@ function pspm_data_editor_OpeningFcn(hObject, eventdata, handles, varargin) end; elseif isnumeric(varargin{1}) set(handles.pnlInput, 'Visible', 'off'); - set(handles.pnlOutput, 'Visible', 'off'); handles.data = varargin{1}; handles.input_mode = 'raw'; guidata(hObject, handles); PlotData(hObject); + + if isfield(handles, 'options') && isfield(handles.options, 'output_file') + set(handles.pnlOutput, 'Visible', 'off'); + end + if isfield(handles, 'options') && isfield(handles.options, 'epoch_file') + set(handles.pnlEpoch, 'Visible', 'off'); + handles = guidata(hObject); + add_epochs(hObject, handles) + end + end; end; uiwait(handles.fgDataEditor); @@ -752,7 +771,6 @@ function SelectedArea(hObject, action) handles = guidata(hObject); if isfield(handles, 'x_data') - start = handles.select.start; if strcmpi(action, 'highlight') pt = get(handles.axData, 'CurrentPoint'); @@ -861,7 +879,7 @@ function InterpolateData(hObject) sd = handles.selected_data; v_pos = find(~isnan(sd)); xd = handles.x_data; -if numel(v_pos)>1 +if numel(v_pos)>=1 epoch_end = xd([v_pos(find(diff(v_pos) > 1)); v_pos(end)]); epoch_start = xd(v_pos([1;find(diff(v_pos) > 1)+1])); @@ -1241,3 +1259,63 @@ function pbSaveOutput_Callback(hObject, eventdata, handles) % eventdata reserved - to be defined in a future version of MATLAB % handles structure with handles and user data (see GUIDATA) CreateOutput(hObject); + + + +function edOpenMissingEpochFilePath_Callback(hObject, eventdata, handles) +% hObject handle to edOpenMissingEpochFilePath (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) + +% Hints: get(hObject,'String') returns contents of edOpenMissingEpochFilePath as text +% str2double(get(hObject,'String')) returns contents of edOpenMissingEpochFilePath as a double +if isempty(handles.epoch_file) + set(hObject, 'String', 'No input specified'); +else + set(hObject, 'String', handles.epoch_file); +end; + +% --- Executes during object creation, after setting all properties. +function edOpenMissingEpochFilePath_CreateFcn(hObject, eventdata, handles) +% hObject handle to edOpenMissingEpochFilePath (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles empty - handles not created until after all CreateFcns called + +% Hint: edit controls usually have a white background on Windows. +% See ISPC and COMPUTER. +if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor')) + set(hObject,'BackgroundColor','white'); +end + + +% --- If Enable == 'on', executes on mouse press in 5 pixel border. +% --- Otherwise, executes on mouse press in 5 pixel border or over edOpenMissingEpochFilePath. +function edOpenMissingEpochFilePath_ButtonDownFcn(hObject, eventdata, handles) +% hObject handle to edOpenMissingEpochFilePath (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) + + +% --- Executes on button press in pbOpenMissingEpochFile. +function pbOpenMissingEpochFile_Callback(hObject, eventdata, handles) +% hObject handle to pbOpenMissingEpochFile (see GCBO) +% eventdata reserved - to be defined in a future version of MATLAB +% handles structure with handles and user data (see GUIDATA) +[file, path] = uigetfile('*.mat', 'Select missing epoch file'); +if file ~= 0 + handles.epoch_file = [ path, file ]; + add_epochs(hObject, handles); +end + +function handles = add_epochs(hObject, handles) +E = load(handles.epoch_file, 'epochs'); +epochs = E.epochs'; +% for each ep add an area as if drawn by the user and add to epoch list +for ep = epochs + handles.select.start = [ ep(1), 0.5 ]; + handles.select.stop = [ ep(2), 0.5 ]; + handles.select.p = 0; + guidata(hObject, handles); + SelectedArea(hObject, 'add'); + UpdateEpochList(hObject); +end diff --git a/src/pspm_pp.m b/src/pspm_pp.m index 199ed86c1..c7baa7a4d 100644 --- a/src/pspm_pp.m +++ b/src/pspm_pp.m @@ -8,15 +8,17 @@ % pspm_pp('simple_qa', datafile, qa, channelnumber, options) % % Currently implemented: -% 'median': medianfilter for SCR -% n: number of timepoints for median filter -% 'butter': 1st order butterworth low pass filter for SCR -% freq: cut off frequency (min 20 Hz) -% 'simple_qa': Simple quality assessment for SCR -% qa: A struct with quality assessment settings -% min: Minimum value in microsiemens -% max: Maximum value in microsiemens -% slope: Maximum slope in microsiemens per second +% 'median': medianfilter for SCR +% n: number of timepoints for median filter +% 'butter': 1st order butterworth low pass filter for SCR +% freq: cut off frequency (min 20 Hz) +% 'simple_qa': Simple quality assessment for SCR +% qa: A struct with quality assessment settings +% min: Minimum value in microsiemens +% max: Maximum value in microsiemens +% slope: Maximum slope in microsiemens per second +% missing_epochs_filename: If provided will create a .mat file with the missing epochs, +% e.g. abc will create abc.mat %__________________________________________________________________________ % % References: For 'simple_qa' method, refer to: diff --git a/src/pspm_simple_qa.m b/src/pspm_simple_qa.m index 99ec1cf72..f9ce135cd 100644 --- a/src/pspm_simple_qa.m +++ b/src/pspm_simple_qa.m @@ -8,14 +8,16 @@ % [sts, out] = pspm_simple_qa(data, sr, options) % % ARGUMENTS: -% data: A numeric vector. Data should be in -% microsiemens. -% sr: Samplerate of the data. This is needed to -% determine the slopes unit. -% options: A struct with algorithm specific settings. -% min: Minimum value in microsiemens (default: 0.05). -% max: Maximum value in microsiemens (default: 60). -% slope: Maximum slope in microsiemens per sec (default: 10). +% data: A numeric vector. Data should be in +% microsiemens. +% sr: Samplerate of the data. This is needed to +% determine the slopes unit. +% options: A struct with algorithm specific settings. +% min: Minimum value in microsiemens (default: 0.05). +% max: Maximum value in microsiemens (default: 60). +% slope: Maximum slope in microsiemens per sec (default: 10). +% missing_epochs_filename: If provided will create a .mat file with the missing epochs, +% e.g. abc will create abc.mat %__________________________________________________________________________ % PsPM 3.2 % (C) 2009-2017 Tobias Moser (University of Zurich) @@ -61,6 +63,8 @@ warning('ID:invalid_input', 'Argument ''options.max'' must be numeric.'); return; elseif ~isnumeric(options.slope) warning('ID:invalid_input', 'Argument ''options.slope'' must be numeric.'); return; +elseif isfield(options, 'missing_epochs_filename') && ~ischar(options.missing_epochs_filename) + warning('ID:invalid_input', 'Argument ''options.missing_epochs_filename'' must be char array.'); return; end % create filters @@ -73,7 +77,33 @@ filt = range_filter & slope_filter; d(filt) = data(filt); +% write epochs to mat if missing_epochs_filename option is present +if isfield(options, 'missing_epochs_filename') + epochs = collect_epochs(filt, sr); + save(options.missing_epochs_filename, 'epochs'); +end + out = d; sts = 1; end + +% construct epochs using filt 0 as offset and 1 as onset +function epochs = collect_epochs(filt, sr) + epochs = []; + epoch_start = NaN; + for i = 1:numel(filt) + if ~filt(i) && isnan(epoch_start) + epoch_start = (i - 1) / sr; + end + if (filt(i) && ~isnan(epoch_start)) + new_epoch = [ epoch_start, (i - 1) / sr ]; + epochs = [ epochs; new_epoch ]; + epoch_start = NaN; + end + end + if ~isnan(epoch_start) + new_epoch = [ epoch_start, numel(filt) / sr ]; + epochs = [ epochs; new_epoch ]; + end +end \ No newline at end of file diff --git a/test/pspm_pp_test.m b/test/pspm_pp_test.m index 9e7e85d53..9e8b38a61 100644 --- a/test/pspm_pp_test.m +++ b/test/pspm_pp_test.m @@ -23,6 +23,7 @@ function invalid_input(this) % perform the other tests with invalid input data this.verifyWarning(@()pspm_pp('foo', fn, 100), 'ID:invalid_input'); this.verifyWarning(@()pspm_pp('butter', fn, 19), 'ID:invalid_freq'); + this.verifyWarning(@()pspm_pp('simple_qa', fn, struct('missing_epochs_filename', 1)), 'ID:invalid_input'); end function median_test(this) @@ -90,6 +91,48 @@ function butter_test(this) %delete testdata delete(fn); end + + + function simple_qa_test(this) + %generate testdata + channels{1}.chantype = 'scr'; + + fn = 'missing_epochs_test_generated_data.mat'; + pspm_testdata_gen(channels, 10, fn); + + %filter one channel + missing_epoch_filename = 'missing_epochs_test_out'; + qa = struct('missing_epochs_filename', missing_epoch_filename); + newfile = pspm_pp('simple_qa', fn, qa); + + [sts, infos, data, filestruct] = pspm_load_data(newfile, 'none'); + + this.verifyTrue(sts == 1, 'the returned file couldn''t be loaded'); + this.verifyTrue(filestruct.numofchan == numel(channels), 'the returned file contains not as many channels as the inputfile'); + + delete(newfile); + + out = load(missing_epoch_filename); + this.verifySize(out.epochs, [ 10, 2 ], 'the written epochs are not of the correct size') + delete(string(missing_epoch_filename) + ".mat"); + + + %no missing epochs filename option + newfile = pspm_pp('simple_qa', fn); + + [sts, infos, data, filestruct] = pspm_load_data(newfile, 'none'); + + this.verifyTrue(sts == 1, 'the returned file couldn''t be loaded'); + this.verifyTrue(filestruct.numofchan == numel(channels), 'the returned file contains not as many channels as the inputfile'); + + delete(newfile); + % test no file exists when not provided + this.verifyError(@()load('missing_epochs_test_out'), 'MATLAB:load:couldNotReadFile'); + + %delete testdata + delete(fn); + end + function overwrite_test(this) % generate test data From 6f7dc0b65a9bbcaf5476c6e2fd20131e76375cfc Mon Sep 17 00:00:00 2001 From: Dominik Bach Date: Mon, 29 Jun 2020 17:56:28 +0100 Subject: [PATCH 08/31] Merge pull request #111 from bachlab/change_request/remove_startsWith_endsWith Remove startsWith and endsWith from all functions --- src/Import/eyelink/import_eyelink.m | 16 ++++++++-------- src/Import/smi/read_smi_events.m | 2 +- src/Import/viewpoint/import_viewpoint.m | 16 +++++++++------- src/backroom/blink_saccade_filtering.m | 2 +- src/backroom/set_blinks_saccades_to_nan.m | 2 +- src/pspm_blink_saccade_filt.m | 7 +++++-- src/pspm_get_viewpoint.m | 2 +- src/pspm_load_data.m | 8 ++++---- src/pspm_pupil_correct_eyelink.m | 2 +- src/pspm_pupil_pp.m | 2 +- test/import_eyelink_test.m | 16 ++++++++-------- test/import_viewpoint_test.m | 15 ++++++++------- test/set_blinks_saccades_to_nan_test.m | 6 +++--- 13 files changed, 51 insertions(+), 45 deletions(-) diff --git a/src/Import/eyelink/import_eyelink.m b/src/Import/eyelink/import_eyelink.m index 44a391b1b..0ea1c6706 100644 --- a/src/Import/eyelink/import_eyelink.m +++ b/src/Import/eyelink/import_eyelink.m @@ -181,7 +181,7 @@ file_info.record_time = '00:00:00'; curr_line = str(linefeeds(line_ctr) + 1 : linefeeds(line_ctr + 1) - 1 - has_backr); tab = sprintf('\t'); - while startsWith(curr_line, '**') + while strncmp(curr_line, '**', numel('**')) if contains(curr_line, 'DATE') colon_idx = strfind(curr_line, ':'); date_part = curr_line(colon_idx + 1 : end); @@ -252,11 +252,11 @@ saccades_R = false(size(dataraw, 1), 1); end - sblink_indices = find(startsWith(messages, 'SBLINK')); - eblink_indices = find(startsWith(messages, 'EBLINK')); - ssacc_indices = find(startsWith(messages, 'SSACC')); - esacc_indices = find(startsWith(messages, 'ESACC')); - msg_indices = startsWith(messages, 'MSG'); + sblink_indices = find(strncmp(messages, 'SBLINK', numel('SBLINK'))); + eblink_indices = find(strncmp(messages, 'EBLINK', numel('EBLINK'))); + ssacc_indices = find(strncmp(messages, 'SSACC', numel('SSACC'))); + esacc_indices = find(strncmp(messages, 'ESACC', numel('ESACC'))); + msg_indices = strncmp(messages, 'MSG', numel('MSG')); for name = {'RECCFG', 'ELCLCFG', 'GAZE_COORDS', 'THRESHOLDS', 'ELCL_', 'PUPIL_DATA_TYPE', '!MODE'} msg_indices = msg_indices & ~contains(messages, name); end @@ -337,7 +337,7 @@ end function [msg_linenums_split, messages_split] = split_messages_to_sessions(msg_linenums, messages) - start_indices = [0 find(startsWith(messages, 'START'))]; + start_indices = [0 find(strncmp(messages, 'START', numel('START')))]; reccfg_indices = find(contains(messages, 'RECCFG')); split_indices = []; @@ -368,7 +368,7 @@ msg = messages{sess_idx}{i}; parts = split(msg); - if startsWith(msg, 'START') + if strncmp(msg, 'START',numel('START')) chan_info{sess_idx}.start_time = str2num(parts{2}); chan_info{sess_idx}.start_msg_idx = prev_n_messages + i; elseif contains(msg, 'GAZE_COORDS') diff --git a/src/Import/smi/read_smi_events.m b/src/Import/smi/read_smi_events.m index 7ae97046a..9ab00bd97 100644 --- a/src/Import/smi/read_smi_events.m +++ b/src/Import/smi/read_smi_events.m @@ -141,7 +141,7 @@ while true if isempty(curr_line) go_forward = 1; - elseif startsWith(curr_line, 'Table Header for') + elseif strncmp(curr_line, 'Table Header for', numel('Table Header for')) go_forward = 2; else break diff --git a/src/Import/viewpoint/import_viewpoint.m b/src/Import/viewpoint/import_viewpoint.m index f289f8eb1..50a9a5fd2 100644 --- a/src/Import/viewpoint/import_viewpoint.m +++ b/src/Import/viewpoint/import_viewpoint.m @@ -96,7 +96,7 @@ [columns, column_ids, line_ctr] = parse_header(str, line_ctr, linefeeds, has_backr); eyesObserved = 'A'; - if any(startsWith(column_ids, 'B')) + if any(strncmp(column_ids, 'B', numel('B'))) eyesObserved = 'AB'; end @@ -137,7 +137,7 @@ file_info.screenSize.ymax = -1; curr_line = str(linefeeds(line_ctr) + 1 : linefeeds(line_ctr + 1) - 1 - has_backr); tab = sprintf('\t'); - while startsWith(curr_line, '3') + while strncmp(curr_line, '3', numel('3')) if contains(curr_line, 'TimeStamp') parts = split(curr_line, tab); date_part = parts{3}; @@ -164,11 +164,11 @@ curr_line = str(linefeeds(line_ctr) + 1 : linefeeds(line_ctr + 1) - 1 - has_backr); tab = sprintf('\t'); n_feeds = numel(linefeeds); - while ~startsWith(curr_line, '10') - if startsWith(curr_line, '6') + while ~strncmp(curr_line, '10', numel('10')) + if strncmp(curr_line, '6', numel('6')) parts = split(curr_line, tab); column_ids = parts(2 : end); - elseif startsWith(curr_line, '5') + elseif strncmp(curr_line, '5',numel('5')) parts = split(curr_line, tab); columns = parts(2 : end); end @@ -235,7 +235,7 @@ read_numeric_columns = ['TYPE'; column_ids]; fmt_array = cell(1, numel(read_numeric_columns)); fmt_array(:) = {'%f'}; - region_indices = find(endsWith(read_numeric_columns, 'RI')); + region_indices = find(~cellfun(@isempty,regexp(read_numeric_columns,'RI$'))); marker_index = find(strcmp(read_numeric_columns, 'MRK')); str_index = find(strcmp(read_numeric_columns, 'STR')); @@ -275,7 +275,9 @@ end elseif msg_type == 14 continue; - elseif (contains(msgline, 'Saccade') || contains(msgline, 'Blink')) && endsWith(msgline, 'sec') + elseif (contains(msgline, 'Saccade') || ... + contains(msgline, 'Blink')) && ... + ~isempty(regexp(msgline,'sec$')) timestamp = str2double(parts{2}); msg = parts{3}; forbeg_idx = strfind(msg, ' for '); diff --git a/src/backroom/blink_saccade_filtering.m b/src/backroom/blink_saccade_filtering.m index 200c578f6..78faac4be 100644 --- a/src/backroom/blink_saccade_filtering.m +++ b/src/backroom/blink_saccade_filtering.m @@ -1,6 +1,6 @@ function [out_data_mat] = blink_saccade_filtering(data_mat, column_names, mask_chans, n_samples, fn_is_left) if nargin == 4 - fn_is_left = @(x) endsWith(x, '_l'); + fn_is_left = @(x) strcmp(x(end-1:end), '_l'); end out_data_mat = expand_mask_chans(data_mat, column_names, mask_chans, n_samples); diff --git a/src/backroom/set_blinks_saccades_to_nan.m b/src/backroom/set_blinks_saccades_to_nan.m index a1d01fe7e..3cd179984 100644 --- a/src/backroom/set_blinks_saccades_to_nan.m +++ b/src/backroom/set_blinks_saccades_to_nan.m @@ -28,7 +28,7 @@ % - all elements in right data columns (except blink/saccade) that correspond to right % blink or saccade rows are set to NaN if nargin == 3 - fn_is_left = @(x) endsWith(x, '_l'); + fn_is_left = @(x) strcmp(x(end-1:end), '_l'); end column_names = cellfun(@(x) lower(x), column_names, 'uni', 0); diff --git a/src/pspm_blink_saccade_filt.m b/src/pspm_blink_saccade_filt.m index eb121a96b..7444f8cbc 100644 --- a/src/pspm_blink_saccade_filt.m +++ b/src/pspm_blink_saccade_filt.m @@ -75,7 +75,8 @@ mask_chans = {}; for i = 1:numel(data) chantype = data{i}.header.chantype; - if startsWith(chantype, 'blink') || startsWith(chantype, 'saccade') + if strncmp(chantype, 'blink', numel('blink')) || ... + strncmp(chantype, 'saccade', numel('saccade')) mask_chans{end + 1} = chantype; data_mat{end + 1} = data{i}.data; column_names{end + 1} = chantype; @@ -84,7 +85,9 @@ n_mask_chans = numel(data_mat); for i = 1:numel(data_user) chantype = data_user{i}.header.chantype; - should_add = options.channel ~= 0 || startsWith(chantype, 'pupil') || startsWith(chantype, 'gaze'); + should_add = options.channel ~= 0 || ... + strncmp(chantype, 'pupil', numel('pupil')) || ... + strncmp(chantype, 'gaze', numel('gaze')); if should_add data_mat{end + 1} = data_user{i}.data; column_names{end + 1} = data_user{i}.header.chantype; diff --git a/src/pspm_get_viewpoint.m b/src/pspm_get_viewpoint.m index e2bbdd2dc..c30ff14ba 100644 --- a/src/pspm_get_viewpoint.m +++ b/src/pspm_get_viewpoint.m @@ -122,7 +122,7 @@ else mask_chans = {'blink_l', 'blink_r', 'saccade_l', 'saccade_r'}; end - data_concat = set_blinks_saccades_to_nan(data_concat, chan_struct, mask_chans, @(x) endsWith(x, '_l')); + data_concat = set_blinks_saccades_to_nan(data_concat, chan_struct, mask_chans, @(x) strcmp(x(end-1:end), '_l')); rmpath(pspm_path('backroom')); num_import_cells = numel(import); diff --git a/src/pspm_load_data.m b/src/pspm_load_data.m index cf977756b..37837c156 100644 --- a/src/pspm_load_data.m +++ b/src/pspm_load_data.m @@ -271,25 +271,25 @@ best_eye = lower(best_eye); chantype_list = cellfun(@(x) x.header.chantype, data, 'uni', false); pupil_channels = cell2mat(cellfun(... - @(chantype) startsWith(chantype, 'pupil'),... + @(chantype) strncmp(chantype, 'pupil',numel('pupil')),... chantype_list,... 'uni',... false... )); preprocessed_channels = cell2mat(cellfun(... - @(chantype) endsWith(chantype, '_pp'),... + @(chantype) strcmp(chantype(end-2:end), '_pp'),... chantype_list,... 'uni',... false... )); combined_channels = cell2mat(cellfun(... - @(chantype) contains(chantype, '_lr_') && endsWith(chantype, '_pp'),... + @(chantype) contains(chantype, '_lr_') && strcmp(chantype(end-2:end), '_pp'),... chantype_list,... 'uni',... false... )); besteye_channels = cell2mat(cellfun(... - @(chantype) endsWith(chantype, ['_' best_eye]) || contains(chantype, ['_' best_eye '_']),... + @(chantype) strcmp(chantype(end-1:end), ['_' best_eye]) || contains(chantype, ['_' best_eye '_']),... chantype_list,... 'uni',... false... diff --git a/src/pspm_pupil_correct_eyelink.m b/src/pspm_pupil_correct_eyelink.m index 79b75a13c..8e67ccf20 100644 --- a/src/pspm_pupil_correct_eyelink.m +++ b/src/pspm_pupil_correct_eyelink.m @@ -264,7 +264,7 @@ % save data % ------------------------------------------------------------------------- pupil_data{1}.data = pupil_corrected; - if ~endsWith(old_chantype, '_pp') + if ~strcmp(old_chantype(end-2:end), '_pp') pupil_data{1}.header.chantype = [old_chantype '_pp']; end channel_str = num2str(options.channel); diff --git a/src/pspm_pupil_pp.m b/src/pspm_pupil_pp.m index dde781ad5..d7db8263a 100644 --- a/src/pspm_pupil_pp.m +++ b/src/pspm_pupil_pp.m @@ -289,7 +289,7 @@ model.filterRawData(); if combining smooth_signal.header.chantype = 'pupil_lr_pp'; - elseif endsWith(data{1}.header.chantype, '_pp') + elseif strcmp(data{1}.header.chantype(end-2:end), '_pp') smooth_signal.header.chantype = data{1}.header.chantype; else smooth_signal.header.chantype = [data{1}.header.chantype '_pp']; diff --git a/test/import_eyelink_test.m b/test/import_eyelink_test.m index 850e599e5..bb52d23ae 100644 --- a/test/import_eyelink_test.m +++ b/test/import_eyelink_test.m @@ -46,21 +46,21 @@ function test_import_eyelink_on_file(this, filepath) is_dataline = ~isempty(str2num(parts{1})); is_msgline = strcmp(parts{1}, 'MSG') && (contains(tline, 'CS') || contains(tline, 'US') || contains(tline, 'TS')); - if startsWith(tline, 'SBLINK L') + if strncmp(tline, 'SBLINK L', numel('SBLINK L')) blink_l = true; - elseif startsWith(tline, 'SBLINK R') + elseif strncmp(tline, 'SBLINK R', numel('SBLINK R')) blink_r = true; - elseif startsWith(tline, 'EBLINK L') + elseif strncmp(tline, 'EBLINK L', numel('EBLINK L')) blink_l = false; - elseif startsWith(tline, 'EBLINK R') + elseif strncmp(tline, 'EBLINK R', numel('EBLINK R')) blink_r = false; - elseif startsWith(tline, 'SSACC L') + elseif strncmp(tline, 'SSACC L', numel('SSACC L')) sacc_l = true; - elseif startsWith(tline, 'SSACC R') + elseif strncmp(tline, 'SSACC R', numel('SSACC R')) sacc_r = true; - elseif startsWith(tline, 'ESACC L') + elseif strncmp(tline, 'ESACC L', numel('ESACC L')) sacc_l = false; - elseif startsWith(tline, 'ESACC R') + elseif strncmp(tline, 'ESACC R', numel('ESACC R')) sacc_r = false; end diff --git a/test/import_viewpoint_test.m b/test/import_viewpoint_test.m index 5cd05f4fd..6439f8f8b 100644 --- a/test/import_viewpoint_test.m +++ b/test/import_viewpoint_test.m @@ -56,7 +56,8 @@ function test_import_viewpoint_on_file(this, fn) for i = 1:numel(eventlines) line = eventlines{i}; parts = split(line, sprintf('\t')); - if any(startsWith(parts{3}, {'A:Blink', 'A:Saccade', 'B:Blink', 'B:Saccade'})) && endsWith(parts{3}, 'sec') + if any(strncmp(parts{3}, {'A:Blink', 'A:Saccade', 'B:Blink', 'B:Saccade'},numel(parts{3})))... + && strcmp(parts{3}(end-2:end), 'sec') tend = to_num(parts{2}); foridx = strfind(line, 'for'); @@ -67,13 +68,13 @@ function test_import_viewpoint_on_file(this, fn) begidx = find(timecol == tbeg); endidx = find(timecol == tend); - if startsWith(parts{3}, 'A:Blink') + if strncmp(parts{3}, 'A:Blink', numel('A:Blink')) this.verifyTrue(all(data{1}.channels(begidx : endidx, blink_A_chan) == 1)); - elseif startsWith(parts{3}, 'B:Blink') + elseif strncmp(parts{3}, 'B:Blink', numel('B:Blink')) this.verifyTrue(all(data{1}.channels(begidx : endidx, blink_B_chan) == 1)); - elseif startsWith(parts{3}, 'A:Saccade') + elseif strncmp(parts{3}, 'A:Saccade', numel('A:Saccade')) this.verifyTrue(all(data{1}.channels(begidx : endidx, sacc_A_chan) == 1)); - elseif startsWith(parts{3}, 'B:Saccade') + elseif strncmp(parts{3}, 'B:Saccade', numel('B:Saccade')) this.verifyTrue(all(data{1}.channels(begidx : endidx, sacc_B_chan) == 1)); end end @@ -126,13 +127,13 @@ function test_import_viewpoint(this) end header = fgetl(fid); tline = fgetl(fid); - while ~startsWith(tline, '10') + while ~strncmp(tline, '10', 2) tline = fgetl(fid); end datalines = {}; eventlines = {}; while isstr(tline) - if startsWith(tline, '10') + if strncmp(tline, '10', 2) datalines{end + 1} = tline; else eventlines{end + 1} = tline; diff --git a/test/set_blinks_saccades_to_nan_test.m b/test/set_blinks_saccades_to_nan_test.m index 721fdeb1a..07e79b342 100644 --- a/test/set_blinks_saccades_to_nan_test.m +++ b/test/set_blinks_saccades_to_nan_test.m @@ -22,7 +22,7 @@ function add(this) function test_only_left_eye(this) column_names = {'blink_l', 'saccade_l', 'pupil_l', 'gaze_x_l', 'gaze_y_l'}; mask_chans = {'blink_l', 'saccade_l'}; - fn_is_left = @(channame) endsWith(channame, 'l'); + fn_is_left = @(channame) strcmp(channame(end), 'l'); data = [false(100, 2), randn(100, 3)]; for i = 1:4 @@ -42,7 +42,7 @@ function test_only_left_eye(this) function test_only_right_eye(this) column_names = {'pupil_r', 'gaze_x_r', 'gaze_y_r', 'blink_r', 'saccade_r'}; mask_chans = {'blink_r', 'saccade_r'}; - fn_is_left = @(channame) endsWith(channame, 'l'); + fn_is_left = @(channame) strcmp(channame(end), 'l'); data = [randn(100, 3), false(100, 2)]; for i = 1:4 @@ -63,7 +63,7 @@ function test_both_eyes(this) column_names = {'Pupil L', 'Pupil R', 'Gaze X L', 'Gaze X R', 'Gaze Y L',... 'Gaze Y R', 'Blink L', 'Blink R', 'Saccade L', 'Saccade R'}; mask_chans = {'Blink L', 'Blink R', 'Saccade L', 'Saccade R'}; - fn_is_left = @(channame) endsWith(channame, ' l'); + fn_is_left = @(channame) strcmp(channame(end-1:end), ' l'); data = [randn(100, 6), false(100, 4)]; for i = 1:4 From 3f9a675159aee1e07fea60ff19a2c95ab84b9902 Mon Sep 17 00:00:00 2001 From: Dominik Bach Date: Tue, 30 Jun 2020 19:29:22 +0100 Subject: [PATCH 09/31] Merge pull request #116 from bachlab/bugfix/pspm_display Add condition to allow plotting any type of marker channel --- src/pspm_display.m | 128 +++++++++++++++++++++++++++++++-------------- 1 file changed, 89 insertions(+), 39 deletions(-) diff --git a/src/pspm_display.m b/src/pspm_display.m index 43da00cf6..d2a7e7d0f 100644 --- a/src/pspm_display.m +++ b/src/pspm_display.m @@ -651,6 +651,7 @@ function ed_y_max_CreateFcn(hObject, eventdata, handles) % ---Initialise------------------------------------------------------------ marker=[]; hbeat=[]; +events=[]; % any other marker channels wave=[]; % get eventchan info @@ -658,10 +659,23 @@ function ed_y_max_CreateFcn(hObject, eventdata, handles) marker=handles.data{handles.prop.eventchans(handles.prop.idevent),1}.data; if get(handles.radio_extra,'Value')==1 handles.prop.event='extra'; - else handles.prop.event='integrated'; + else + handles.prop.event='integrated'; end elseif not(isempty(handles.prop.eventchans)) && not(handles.prop.eventchans(handles.prop.idevent)==0) && strcmp(handles.data{handles.prop.eventchans(handles.prop.idevent),1}.header.chantype,'hb') hbeat=handles.data{handles.prop.eventchans(handles.prop.idevent),1}.data; + if get(handles.radio_extra,'Value')==1 + handles.prop.event='extra'; + else + handles.prop.event='integrated'; + end +elseif not(isempty(handles.prop.eventchans)) && not(handles.prop.eventchans(handles.prop.idevent)==0) + events=handles.data{handles.prop.eventchans(handles.prop.idevent),1}.data; + if get(handles.radio_extra,'Value')==1 + handles.prop.event='extra'; + else + handles.prop.event='integrated'; + end end % get wave chan info @@ -672,7 +686,7 @@ function ed_y_max_CreateFcn(hObject, eventdata, handles) % plotting if no channel is selected % ------------------------------------------------------------------------- -if isempty(marker) && isempty(wave) && isempty(hbeat) +if isempty(marker) && isempty(wave) && isempty(hbeat) && isempty(events) hold off x=1; y=1; @@ -680,7 +694,7 @@ function ed_y_max_CreateFcn(hObject, eventdata, handles) text(x,y,' nothing to display - please select channel ','FontSize',20,'BackgroundColor','k',... 'Color','w','HorizontalAlignment','Center','VerticalAlignment',... 'Middle') -elseif not(isempty(marker)) || not(isempty(wave)) || not(isempty(hbeat)) +elseif not(isempty(marker)) || not(isempty(wave)) || not(isempty(hbeat)) || not(isempty(events)) % ---prepare ecg----------------------------------------------------------- if not(isempty(handles.prop.wave)) % only if there is wave info @@ -719,53 +733,76 @@ function ed_y_max_CreateFcn(hObject, eventdata, handles) base(1)=min(wave)-.1*min(wave); base(2)=min(wave)-(max(wave)-min(wave)); - if strcmp(handles.prop.event,'hb') - R=zeros(size(wave)); - R(R==0)=NaN; + if not(isempty(hbeat)) + + hbeat=round(hbeat*sr.wave); - if not(isempty(hbeat)) - hbeat=round(hbeat*sr.wave); - else - warning('No information on heartbeats. Plotting not possible'); - end + HBEAT = nan(size(wave)); - if strcmp(handles.prop.wave,'ecg') - R(hbeat,1)=max(wave); - hold on ; stem(y,R,'r') - else - R(hbeat,1)=max(base(1)); - hold on ; h=stem(y,R,'r'); - hbase=get(h,'Baseline'); - set(hbase,'Basevalue',base(2)); - + if strcmp(handles.prop.event,'extra') + HBEAT(hbeat,1)=min(wave)-.5; + elseif strcmp(handles.prop.event,'integrated') + temp=wave(hbeat,1); + temp(isnan(temp)) = median(temp,'omitnan'); + HBEAT(hbeat,1)=temp; + end + + hold on ; h=stem(y,HBEAT,'ro'); + hbase=get(h,'Baseline'); + + if strcmp(handles.prop.event,'extra') + set(hbase,'BaseValue',base(2),'Visible','off'); + elseif strcmp(handles.prop.event,'integrated') + set(hbase,'BaseValue',base(1),'Visible','off'); end - elseif strcmp(handles.prop.event,'integrated') || strcmp(handles.prop.event,'extra') + elseif not(isempty(marker)) marker=round(marker*sr.wave); if marker(1,1)==0 marker(1,1)=1; end marker=marker(marker~=0); - MARKER=zeros(size(wave)); + MARKER=nan(size(wave)); if strcmp(handles.prop.event,'extra') MARKER(marker,1)=min(wave)-.5; elseif strcmp(handles.prop.event,'integrated') temp=wave(marker,1); - median_non_nan_vals = median(temp,'omitnan'); + temp(isnan(temp)) = median(temp,'omitnan'); MARKER(marker,1)=temp; - MARKER(isnan(MARKER))=median_non_nan_vals; end - MARKER(MARKER==0)=NaN; hold on ; h=stem(y,MARKER,'ro'); hbase=get(h,'Baseline'); if strcmp(handles.prop.event,'extra') set(hbase,'BaseValue',base(2),'Visible','off'); - else set(hbase,'BaseValue',base(1),'Visible','off'); + elseif strcmp(handles.prop.event,'integrated') + set(hbase,'BaseValue',base(1),'Visible','off'); + end + elseif not(isempty(events)) + + events=round(events*sr.wave); + + EVENTS = nan(size(wave)); + + if strcmp(handles.prop.event,'extra') + EVENTS(events,1)=min(wave)-.5; + elseif strcmp(handles.prop.event,'integrated') + temp=wave(events,1); + temp(isnan(temp)) = median(temp,'omitnan'); + EVENTS(events,1)=temp; + end + + hold on ; h=stem(y,EVENTS,'r'); + hbase=get(h,'Baseline'); + + if strcmp(handles.prop.event,'extra') + set(hbase,'BaseValue',base(2),'Visible','off'); + elseif strcmp(handles.prop.event,'integrated') + set(hbase,'BaseValue',base(1),'Visible','off'); end end @@ -792,35 +829,48 @@ function ed_y_max_CreateFcn(hObject, eventdata, handles) end xlabel(' Time in seconds [s] ','Fontsize',16); - - if (strcmp(handles.prop.event,'integrated')||strcmp(handles.prop.event,'extra')) && not(strcmp(handles.prop.wave,'none')) + + if not(isempty(marker)) legend(handles.prop.wave,'marker') - elseif not(strcmp(handles.prop.event,'none')) && not(strcmp(handles.prop.wave,'none')) - legend(handles.prop.wave,handles.prop.event) + elseif not(isempty(hbeat)) + legend(handles.prop.wave,'heartbeats') + elseif not(isempty(events)) + legend(handles.prop.wave,[handles.event_listbox.String{handles.prop.idevent},' events']) elseif not(strcmp(handles.prop.event,'none')) && not(strcmp(handles.prop.wave,'none')) legend(handles.prop.wave) + else + legend(handles.prop.wave,'unknown events') end % plotting if only event channel is selected % ------------------------------------------------------------------------- - elseif isempty(wave) && (not(isempty(hbeat)) || not(isempty(marker))) - if strcmp(handles.prop.event,'hb') + elseif isempty(wave) && (not(isempty(hbeat)) || not(isempty(marker)) || not(isempty(events))) + if not(isempty(hbeat)) MARKER=diff(hbeat); plot(MARKER,'ro') ylabel('duration of ibi [s]','Fontsize',14) - elseif strcmp(handles.prop.event,'extra') || strcmp(handles.prop.event,'integrated') + elseif not(isempty(marker)) MARKER=diff(marker); stem(MARKER,'r') - else MARKER=[]; + ylabel('inter-marker duration [s]','Fontsize',14) + elseif not(isempty(events)) + MARKER=diff(events); + plot(MARKER,'ro') + ylabel('inter-event duration [s]','Fontsize',14) + else + MARKER=[]; end xlabel('Time in seconds [s] ','Fontsize',16); - if strcmp(handles.prop.event,'integrated') || strcmp(handles.prop.event,'extra') + if not(isempty(marker)) legend('marker') - elseif strcmp(handles.prop.event,'hb') + elseif not(isempty(hbeat)) legend('heartbeats') - else legend('unknown marker') + elseif not(isempty(events)) + legend([handles.event_listbox.String{handles.prop.idevent},' events']) + else + legend('unknown events') end end end @@ -947,10 +997,10 @@ function push_plot_Callback(hObject, eventdata, handles) handles.prop.event=handles.prop.event{handles.prop.idevent,1}; % ---deactivate marker buttons if necessary-------------------------------- -if handles.prop.idevent==1 || strcmp(handles.prop.event,'hb') || handles.prop.idwave==1 +if handles.prop.idevent==1 || handles.prop.idwave==1 set(handles.radio_int,'Enable','Off'); set(handles.radio_extra,'Enable','Off'); -elseif not(handles.prop.idevent==1) && strcmp(handles.prop.event,'marker') +else set(handles.radio_int,'Enable','On'); set(handles.radio_extra,'Enable','On'); end From a142c3581b1df6d24c950158ed5064ae4e5ae4d0 Mon Sep 17 00:00:00 2001 From: Dominik Bach Date: Wed, 1 Jul 2020 12:17:20 +0100 Subject: [PATCH 10/31] Merge pull request #131 from bachlab/bugfix/split_session_rand_ITI Add an option to take into account random ITIs --- src/pspm_split_sessions.m | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/pspm_split_sessions.m b/src/pspm_split_sessions.m index d7770cf41..b75c3b265 100644 --- a/src/pspm_split_sessions.m +++ b/src/pspm_split_sessions.m @@ -30,6 +30,10 @@ % Default = 0 % options.verbose Tell the function to display information % about the state of processing. Default = 0 +% options.randomITI Tell the function to use all the markers to +% evaluate the mean distance between them. +% Usefull for random ITI since it reduce the +% variance. Default = 0 % % REMARK for suffix and prefix: % The prefix and suffix intervals will only be applied to data - @@ -90,6 +94,8 @@ options.min_break_ratio = settings.split.min_break_ratio; end +try options.randomITI; catch, options.randomITI = 0; end + % check input arguments % ------------------------------------------------------------------------- if nargin < 1 @@ -124,6 +130,10 @@ warning('ID:invalid_input', 'options.splitpoints has to be numeric.'); return; end +if ~isnumeric(options.randomITI) || ~ismember(options.randomITI, [0, 1]) + warning('ID:invalid_input', 'options.randomITI has to be numeric and equal to 0 or 1.'); return; +end + % work on all data files % ------------------------------------------------------------------------- for d = 1:numel(D) @@ -185,7 +195,7 @@ % relevant data within the mean space % add global mean space - if sta == sto + if sta == sto || options.randomITI mean_space = mean(diff(mrk)); else mean_space = mean(diff(mrk(sta:sto))); From cf0f92a14cc42f86f3f742b397cb4478976d3dc2 Mon Sep 17 00:00:00 2001 From: irojkov-ph <56234933+irojkov-ph@users.noreply.github.com> Date: Mon, 27 Jul 2020 13:55:16 +0200 Subject: [PATCH 11/31] Merge pull request #134 from bachlab/fix/descending-marker-events fix(descending marker events): update index for descending events --- src/pspm_get_events.m | 2 +- test/pspm_get_marker_test.m | 29 +++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/pspm_get_events.m b/src/pspm_get_events.m index aefb0f5d9..8d09be9c5 100644 --- a/src/pspm_get_events.m +++ b/src/pspm_get_events.m @@ -90,7 +90,7 @@ mPos = lo2hi+3; elseif isfield(import, 'flank') && strcmpi(import.flank, 'descending') import.data = (hi2lo+1)./import.sr; - mPos = hi2lo+2; + mPos = hi2lo+3; elseif numel(lo2hi) == numel(hi2lo) % only use mean if amount of minima corresponds to amount of maxima % otherwise output a warning diff --git a/test/pspm_get_marker_test.m b/test/pspm_get_marker_test.m index b5943c767..b89989afe 100644 --- a/test/pspm_get_marker_test.m +++ b/test/pspm_get_marker_test.m @@ -3,10 +3,15 @@ % unittest class for the pspm_get_marker function %__________________________________________________________________________ % SCRalyze TestEnvironment -% (C) 2013 Linus Rüttimann (University of Zurich) +% (C) 2013 Linus Rüttimann (University of Zurich) + + properties (TestParameter) + flank = { 'descending', 'ascending' }; + sr = { 1, 2 }; + end methods (Test) - function test(this) + function timestamps(this) import.sr = 1; import.data = 1:10; import.marker = 'timestamps'; @@ -20,6 +25,26 @@ function test(this) this.verifyEqual(data.header.sr, 1); end + + function continuous(this, flank, sr) + import.sr = sr; + import.data = [ 42, 42, 84, 84, 84, 42, 42, 42, 84, 42 ]; + import.marker = 'continuous'; + import.flank = flank; + [sts, data] = pspm_get_marker(import); + + expected = struct(... + 'descending', [ 6; 10 ], ... + 'ascending', [ 3; 9 ]... + ); + + this.verifyEqual(sts, 1); + this.verifyEqual(data.data, expected.(flank) ./ sr); + this.verifyTrue(strcmpi(data.header.chantype, 'marker')); + this.verifyTrue(strcmpi(data.header.units, 'events')); + this.verifyEqual(data.header.sr, 1); + end + end end \ No newline at end of file From 33a1fa3bf9d6a7bd2825dc07335a9bff1b7172d6 Mon Sep 17 00:00:00 2001 From: Sam Maxwell Date: Wed, 29 Jul 2020 17:28:19 +0100 Subject: [PATCH 12/31] Merge pull request #136 from bachlab/bugfix/import_viewpoint import_viewpoint doesn't import the right number of datapoints --- src/Import/viewpoint/import_viewpoint.m | 2 +- test/import_viewpoint_test.m | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Import/viewpoint/import_viewpoint.m b/src/Import/viewpoint/import_viewpoint.m index 50a9a5fd2..7cb546b55 100644 --- a/src/Import/viewpoint/import_viewpoint.m +++ b/src/Import/viewpoint/import_viewpoint.m @@ -114,7 +114,7 @@ begidx = linefeeds(msg_line) + 1; str(begidx : begidx + 1) = '/'; end - C = textscan(str, fmt_str, 'Delimiter', '\t', 'CollectOutput', 1, 'CommentStyle', '//'); + C = textscan(str, fmt_str, 'CollectOutput', 1, 'CommentStyle', '//'); dataraw = C{1}; marker = C{2}; diff --git a/test/import_viewpoint_test.m b/test/import_viewpoint_test.m index 6439f8f8b..1b43b73d8 100644 --- a/test/import_viewpoint_test.m +++ b/test/import_viewpoint_test.m @@ -83,7 +83,7 @@ function test_import_viewpoint_on_file(this, fn) % --------------------------------------------------------------------------- msg_counter = 1; for line = datalines - parts = split(line, sprintf('\t')); + parts = strsplit(line{1},'\t'); msg = parts{marker_index}; if ~isempty(msg) tbeg = to_num(parts{2}); From 4c650b7d990b0edf45f7b3d842c23aa99173ed76 Mon Sep 17 00:00:00 2001 From: irojkov-ph <56234933+irojkov-ph@users.noreply.github.com> Date: Wed, 19 Aug 2020 08:05:01 +0200 Subject: [PATCH 13/31] Merge pull request #143 from bachlab/bugfix/pspm_display_load_data pspm_display crashes when loading data --- src/pspm_display.m | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/pspm_display.m b/src/pspm_display.m index d2a7e7d0f..6faebc6a4 100644 --- a/src/pspm_display.m +++ b/src/pspm_display.m @@ -411,10 +411,17 @@ function load_Callback(hObject, eventdata, handles) handles.name=filename; guidata(hObject, handles); + % ---set wave and event channel value to none --------------------- + handles.prop.wave='none'; + set(handles.wave_listbox,'Value',1) + + handles.prop.event='none'; + set(handles.event_listbox,'Value',1) + % ---add text to wave listbox-------------------------------------- listitems{1,1}='none'; - handles.prop.wavechans(1)=0; + handles.prop.wavechans(1)=0; j=2; for k=1:length(handles.data) if any(strcmp(handles.data{k,1}.header.chantype,handles.prop.setwave)) @@ -836,10 +843,10 @@ function ed_y_max_CreateFcn(hObject, eventdata, handles) legend(handles.prop.wave,'heartbeats') elseif not(isempty(events)) legend(handles.prop.wave,[handles.event_listbox.String{handles.prop.idevent},' events']) - elseif not(strcmp(handles.prop.event,'none')) && not(strcmp(handles.prop.wave,'none')) - legend(handles.prop.wave) - else + elseif not(strcmp(handles.prop.event,'none')) legend(handles.prop.wave,'unknown events') + else + legend(handles.prop.wave) end % plotting if only event channel is selected From 3cd268f920765b01b6817753e10d6849bca79486 Mon Sep 17 00:00:00 2001 From: Sam Maxwell Date: Thu, 20 Aug 2020 14:48:40 +0100 Subject: [PATCH 14/31] Merge pull request #132 from bachlab/feat/scanpath-glm-gui feat(scanpath-glm): add SPS to first level glm drop down --- doc/PsPM.bib | 27 ++ doc/PsPM_Developers_Guide.lyx | 2 +- doc/PsPM_Manual.lyx | 296 +++++++++++++++++- src/pspm.fig | Bin 26706 -> 25494 bytes src/pspm.m | 10 +- src/pspm_bf_spsrf_box.m | 10 +- src/pspm_bf_spsrf_gamma.m | 11 +- src/pspm_cfg/pspm_cfg_data_preprocessing.m | 2 +- src/pspm_cfg/pspm_cfg_gaze_convert.m | 144 +++++++++ src/pspm_cfg/pspm_cfg_glm_hp_fc.m | 2 +- src/pspm_cfg/pspm_cfg_glm_sps.m | 46 ++- src/pspm_cfg/pspm_cfg_pupil_size_convert.m | 78 +++++ src/pspm_cfg/pspm_cfg_run_gaze_convert.m | 25 ++ .../pspm_cfg_run_pupil_size_convert.m | 18 ++ src/pspm_compute_visual_angle.m | 47 +-- src/pspm_compute_visual_angle_core.m | 80 +++++ src/pspm_convert_gaze_distance.m | 165 ++++++++++ src/pspm_convert_pixel2unit.m | 4 +- src/pspm_convert_visangle2sps.m | 96 +++--- src/pspm_convert_visangle2sps_core.m | 27 ++ src/{pspm_get_sp_speed.m => pspm_get_sps.m} | 12 +- src/pspm_get_sps_l.m | 32 ++ src/pspm_get_sps_r.m | 32 ++ src/pspm_glm.m | 4 + src/pspm_init.m | 22 +- src/pspm_write_channel.m | 17 +- test/pspm_convert_gaze_distance_test.m | 116 +++++++ test/pspm_get_sps_test.m | 33 ++ test/pspm_test.m | 2 + test/pspm_testdata_gen.m | 22 +- test/pspm_write_channel_test.m | 35 +++ 31 files changed, 1278 insertions(+), 139 deletions(-) create mode 100644 src/pspm_cfg/pspm_cfg_gaze_convert.m create mode 100644 src/pspm_cfg/pspm_cfg_pupil_size_convert.m create mode 100644 src/pspm_cfg/pspm_cfg_run_gaze_convert.m create mode 100644 src/pspm_cfg/pspm_cfg_run_pupil_size_convert.m create mode 100644 src/pspm_compute_visual_angle_core.m create mode 100644 src/pspm_convert_gaze_distance.m create mode 100644 src/pspm_convert_visangle2sps_core.m rename src/{pspm_get_sp_speed.m => pspm_get_sps.m} (59%) create mode 100644 src/pspm_get_sps_l.m create mode 100644 src/pspm_get_sps_r.m create mode 100644 test/pspm_convert_gaze_distance_test.m create mode 100644 test/pspm_get_sps_test.m diff --git a/doc/PsPM.bib b/doc/PsPM.bib index 24cdda7bc..8308b17b2 100644 --- a/doc/PsPM.bib +++ b/doc/PsPM.bib @@ -10,7 +10,34 @@ @comment{jabref-meta: databaseType:bibtex;} +@techreport{xia:2020, + type = {preprint}, + title = {Saccadic {Scanpath} {Length}: {An} {Index} for {Human} {Threat} {Conditioning}}, + shorttitle = {Saccadic {Scanpath} {Length}}, + url = {https://osf.io/qtpve}, + abstract = {Threat-conditioned cues are thought to capture overt attention in a bottom-up process. Quantification of this phenomenon typically relies on cue competition paradigms. Here, we sought to exploit gaze patterns during presentation of a visual conditioned stimulus only, in order to quantify threat conditioning. To this end, we capitalise on a summary statistic of visual search during CS presentation, scanpath length. In a simple delayed threat conditioning paradigm with full-screen monochrome conditioned stimuli (CS), we observed shorter scanpath length during CS+ compared to CS- presentation. Retrodictive validity, i.e. effect size to distinguish CS+ and CS-, was maximised by considering a 2-s time window before US onset. Taking into account the shape of the scan speed response resulted in similar effect size. The mechanism underlying shorter scanpath length appeared to be longer fixation duration and more fixation on the screen center during CS+ relative to CS- presentation. These findings were replicated in a second experiment with similar setup, and further confirmed in a third experiment using full-screen fractals as CS. This experiment included an extinction session during which scanpath differences disappeared. In a fourth experiment with auditory CS and instruction to fixate screen centre, no scanpath length differences were observed. In conclusion, our study suggests scanpath length as a visual search summary statistic, which may be used as complementary measure to quantify threat conditioning with retrodictive validity similar to that of skin conductance responses.}, + urldate = {2020-07-09}, + institution = {PsyArXiv}, + author = {Xia, Yanfang and Melinscak, Filip and Bach, Dominik R}, + month = feb, + year = {2020}, + doi = {10.31234/osf.io/qtpve}, +} +@article{schutz:2011, + Author = {Schutz, A. C. and Braun, D. I. and Gegenfurtner, K. R.}, + title = {Eye movements and perception: {A} selective review}, + Volume = {11}, + issn = {1534-7362}, + shorttitle = {Eye movements and perception}, + url = {http://jov.arvojournals.org/Article.aspx?doi=10.1167/11.5.9}, + doi = {10.1167/11.5.9}, + Number = {5}, + urldate = {2019-12-16}, + Journal = {Journal of Vision}, + Year = {2011}, + pages = {1--30}, +} @article{Homan:2017aa, Author = {Homan, Philipp and Lin, Qi and Murrough, James W and Soleimani, Laili and Bach, Dominik R and Clem, Roger L and Schiller, Daniela}, diff --git a/doc/PsPM_Developers_Guide.lyx b/doc/PsPM_Developers_Guide.lyx index 140ed842e..a3ced3e5d 100644 --- a/doc/PsPM_Developers_Guide.lyx +++ b/doc/PsPM_Developers_Guide.lyx @@ -24238,7 +24238,7 @@ Eshref Yozdemir \begin_inset Text \begin_layout Plain Layout -pspm_get_sp_speed +pspm_get_sps \end_layout \end_inset diff --git a/doc/PsPM_Manual.lyx b/doc/PsPM_Manual.lyx index 6e62f0044..90e1a6815 100644 --- a/doc/PsPM_Manual.lyx +++ b/doc/PsPM_Manual.lyx @@ -5028,6 +5028,159 @@ model each trial as separate event type such that the response latency can be estimated per trial, rather than per condition \end_layout +\end_deeper +\begin_layout Section +Scanpath speed (SPS) model +\end_layout + +\begin_layout Subsection +General +\end_layout + +\begin_layout Standard +Various salient stimuli, e.g. + threat-conditioned cues, capture overt attention in a bottom-up process + +\begin_inset CommandInset citation +LatexCommand cite +key "schutz:2011" +literal "false" + +\end_inset + +, as shown in multiple stimulus competition paradigms using aversive cues. + This yields a possibility to assess threat memory by overt attention. + In cue competition paradigms, overt attention is usually measured as gaze + patterns; for example, fixation duration, saccade initiation latency, and + number of erroneous saccades. + Here, we implement a model that captures gaze patterns during exclusive + presentation of a single cue, by assessing scanpath speed, the length of + scanpath per time unit, quantified as degree visual angle per second +\begin_inset CommandInset citation +LatexCommand cite +key "xia:2020" +literal "false" + +\end_inset + +. + In our publication, we refer to scanpath length, which is the integral + of scanpath speed over time. + Since we used constant cue presentation times in our publication, average + scanpath speed and scanpath length were identical up to a multiplicative + constant. +\end_layout + +\begin_layout Subsection +Model for fear-conditioned SPS +\end_layout + +\begin_layout Standard +Scanpath speed/length allow differentiating CS+ and CS- in fear-conditioning + paradigms with visual cues, and without fixation instructions or fixation + cross. + PsPM implements two simple models that build on the GLM inversion algorithm + (see +\begin_inset CommandInset ref +LatexCommand ref +reference "subsec:General-linear-convolution" +plural "false" +caps "false" +noprefix "false" + +\end_inset + +). + The first model is to compute average scanpath speed during a 2-s time + window before US (shock) delivery. + This was validated on three independent samples in +\begin_inset CommandInset citation +LatexCommand cite +key "xia:2020" +literal "false" + +\end_inset + +. + The model is implemented with a simple boxcar response function without + mean-centering and is the default option. + Parameter estimates can be interpreted as average scan path speed per second, + and if they are multiplied with 2 then the resulting values can be interpreted + as scan path length during the 2 s preceding the US. + We also implement a gamma RF that describes the shape of the scanpath speed + during the CS-US interval better than the boxcar function, but did not + yield higher retrodictive validity in inferring the CS-/+ difference. + This RF was fitted to the data of 21 participants and validated in an independe +nt sample; it is described by a gamma probability density function: +\begin_inset Formula +\[ +y=\frac{A}{\mu^{k}\Gamma(k)}(t-t_{0})^{k-1}e^{-\frac{t-t_{0}}{\mu}} +\] + +\end_inset + + with parameters +\begin_inset Formula $k=10.0913,µ=0.4213,t0=-1.9020+(SOA–3)$ +\end_inset + + +\begin_inset CommandInset citation +LatexCommand cite +key "xia:2020" +literal "false" + +\end_inset + +, where +\begin_inset Formula $SOA$ +\end_inset + + is the duration of the interval between CS onset and US onset in seconds. + +\end_layout + +\begin_layout Subsection +Data preconditioning +\end_layout + +\begin_layout Standard +PsPM provides procedures to import scan path speed directly. + More commonly, it is convenient to import gaze coordinates in pixels, distance + units, or visual angle, and convert them to scanpath speed. + This requires interpolation of missing values which is done automatically. + Conversion from pixels to mm or visual angle in degree is also possible. + No filtering or smoothing is required for both RF during model inversion + with GLM. +\end_layout + +\begin_layout Subsection +Recommendations +\end_layout + +\begin_layout Itemize +Experimental design: +\end_layout + +\begin_deeper +\begin_layout Itemize +use visual cues during fear conditioning and avoid fixation guides. +\end_layout + +\begin_layout Itemize +use a SOA longer than 2 s. + Tested SOAs are 3.0 s and 3.5 s. +\end_layout + +\end_deeper +\begin_layout Itemize +Model setup: +\end_layout + +\begin_deeper +\begin_layout Itemize +use the default boxcar function and specify custom SOA. +\end_layout + \end_deeper \begin_layout Part User Guide @@ -5916,7 +6069,7 @@ The default value has been changed to 0 in PsPM revision r803 to reduce \begin_inset CommandInset ref LatexCommand ref -reference "subsec:Pupil-Preprocessing" +reference "subsec:Pupil-Size-Preprocessing" plural "false" caps "false" noprefix "false" @@ -8629,10 +8782,10 @@ replace: Replace previously existing preprocessed channel. \end_layout \begin_layout Subsection -Pupil Preprocessing +Pupil Size Preprocessing \begin_inset CommandInset label LatexCommand label -name "subsec:Pupil-Preprocessing" +name "subsec:Pupil-Size-Preprocessing" \end_inset @@ -9340,6 +9493,117 @@ Defines whether the processed data should be added as a new channel or replace (default: add) \end_layout +\begin_layout Subsection +Gaze Preprocessing +\begin_inset CommandInset label +LatexCommand label +name "subsec:Gaze-Preprocessing" + +\end_inset + + +\end_layout + +\begin_layout Standard + +\shape italic +Related function: +\shape default + +\family typewriter +pspm_convert_gaze_distance +\end_layout + +\begin_layout Standard +Convert gaze data from degree or distance units such as pixels or mm into + degree data or scanpath speed. + The gaze data must be oriented such that the point 0,0 relates the bottom-left + of the screen, and the maximal x and y points relate to the top-right of + the screen. + These functions do not accept input channels, they will seacrch the provided + data file for channels with the gaze_[x|y]_[l|r] chantype in the specified + from unit. +\end_layout + +\begin_layout Subsubsection* +Data file +\end_layout + +\begin_layout Standard +Specify the PsPM datafile containing the gaze recordings. +\end_layout + +\begin_layout Subsubsection* +Conversion +\end_layout + +\begin_layout Paragraph* +Degree To Scanpath Speed +\end_layout + +\begin_layout Standard +Convert existing degree data to scanpath speed, stored as chantype: sps_[l|r] +\end_layout + +\begin_layout Paragraph* +Distance To Degree +\end_layout + +\begin_layout Standard +Convert distance x/y coordinate data to degrees stored as chantype: gaze_[x|y]_[ +l|r], unit: degree +\end_layout + +\begin_layout Itemize +From - unit to covert from, [ pixel, mm, cm, inches, m ] +\end_layout + +\begin_layout Itemize +Width - The width of the screen in mm +\end_layout + +\begin_layout Itemize +Height - The height of the screen in mm +\end_layout + +\begin_layout Itemize +Distance - The distance to the screen from the subjects eyes, in mm +\end_layout + +\begin_layout Paragraph* +Distance To Scanpath Speed +\end_layout + +\begin_layout Standard +Convert distance x/y coordinate data to scanpath speed, stored as chantype: + sps_[l|r] +\end_layout + +\begin_layout Itemize +From - unit to covert from, [ pixel, mm, cm, inches, m ] +\end_layout + +\begin_layout Itemize +Width - The width of the screen in mm +\end_layout + +\begin_layout Itemize +Height - The height of the screen in mm +\end_layout + +\begin_layout Itemize +Distance - The distance to the screen from the subjects eyes, in mm +\end_layout + +\begin_layout Subsubsection* +Channel Action +\end_layout + +\begin_layout Standard +Defines whether the processed data should be added as a new channel or replace + the last existing channel of the same chantype and unit. +\end_layout + \begin_layout Section First level \end_layout @@ -10170,8 +10434,8 @@ SOA \begin_layout Standard Specify custom SOA for response function. - Tested values are 3.5s, 4s and 6s. - Default: 3.5s + Tested values are 3.5 s, 4 s and 6 s. + Default: 3.5 s \end_layout \begin_layout Subsection @@ -10650,19 +10914,33 @@ Basis functions for the peripheral model. \end_layout \begin_layout Paragraph* -SPSRF_BOX +Average scanpath speed \end_layout \begin_layout Standard -SPSRF with normal boxfunction (default). +This option implements a boxcar function over 2 s before the US time point, + and yields the averaged scan path speed over that interval (i.e. + the scan path length over that interval, divided by 2 s). + (default). \end_layout \begin_layout Paragraph* -SPSRF_GAMMA +SPSRF_FC \end_layout \begin_layout Standard -SEBRF with gammafunction. +This option implements a gamma function for fear-conditioned scan path speed + responses, time-locked to the end of the CS-US interval. +\end_layout + +\begin_layout Paragraph* +SOA +\end_layout + +\begin_layout Standard +Specify custom SOA for response function. + Tested values are 3.0 s and 3.5 s. + Default: 3.5 s \end_layout \begin_layout Subsection diff --git a/src/pspm.fig b/src/pspm.fig index 515b222200af31c69b48600580a53d307dc29884..6056db80998a0afc3ca4a33310de293b4086c49a 100644 GIT binary patch literal 25494 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 literal 26706 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 diff --git a/src/pspm.m b/src/pspm.m index 90c5daf92..83640ad9b 100644 --- a/src/pspm.m +++ b/src/pspm.m @@ -285,6 +285,8 @@ function GLM_Callback(hObject, eventdata, handles) cfg_add_module('pspm.first_level.resp.glm_rfr_e'); case 9 cfg_add_module('pspm.first_level.sebr.glm_sebr'); + case 10 + cfg_add_module('pspm.first_level.sps.glm_sps'); end; @@ -316,10 +318,14 @@ function ppDataPreprocessing_Callback(hObject, eventdata, handles) case 7 cfg_add_module('pspm.data_preprocessing.pp_pupil.pupil_preprocess'); case 8 - cfg_add_module('pspm.tools.convert_data'); + cfg_add_module('pspm.data_preprocessing.pupil_size_convert'); case 9 - cfg_add_module('pspm.data_preprocessing.pp_emg.find_sounds'); + cfg_add_module('pspm.data_preprocessing.gaze_convert'); case 10 + cfg_add_module('pspm.tools.convert_data'); + case 11 + cfg_add_module('pspm.data_preprocessing.pp_emg.find_sounds'); + case 12 cfg_add_module('pspm.data_preprocessing.pp_emg.pp_emg_data'); end; diff --git a/src/pspm_bf_spsrf_box.m b/src/pspm_bf_spsrf_box.m index 40a6ce2a7..c64fa64a0 100644 --- a/src/pspm_bf_spsrf_box.m +++ b/src/pspm_bf_spsrf_box.m @@ -1,5 +1,7 @@ function [bs, x] = pspm_bf_spsrf_box(varargin) -% pspm_bf_spsrf_box basis function dependent on SOA +% pspm_bf_spsrf_box constructs a boxcar function to produce the averaged +% scanpath speed over a 2-s time window in the end of SOA, which equals +% to scanpath length over a 2-s time window divided by 2/s % % FORMAT: [bs, x] = pspm_bf_spsrf_box(td, soa) % OR: [bs, x] = pspm_bf_spsrf_box([td, soa]) @@ -7,6 +9,10 @@ % FORMAT: [bf p] = pspm_bf_spsrf_box(td, soa) % with td: time resolution in s % +% REFERENCE +% (1) Xia Y, Melinscak F, Bach DR (2020) +% Saccadic Scanpath Length: An Index for Human Threat Conditioning +% Behavioral Research Methods (submitted) %________________________________________________________________________ % PsPM 4.0 @@ -27,7 +33,7 @@ end; -%create boder of interval +% create border of interval stop = soa; start = soa-2; start_idx = floor(start/td); diff --git a/src/pspm_bf_spsrf_gamma.m b/src/pspm_bf_spsrf_gamma.m index a7d1fbc3c..3310d11e9 100644 --- a/src/pspm_bf_spsrf_gamma.m +++ b/src/pspm_bf_spsrf_gamma.m @@ -1,6 +1,7 @@ function [bs, t] = pspm_bf_spsrf_gamma(varargin) -% pspm_bf_spsrf_box basis function with a total duration of 10 seconds -% and a shift of (SOA-3) seconds (see reference). +% pspm_bf_spsrf_gamma constructs a gamma probability density function for +% scanpath speed responses with a total duration of 10 seconds and a shift +% of (SOA - 3) seconds. % % FORMAT: [bf p] = pspm_bf_spsrf_gamma(td,soa,p) OR % [bf p] = pspm_bf_spsrf_gamma([td,soa,p]) @@ -9,7 +10,11 @@ % p(2) = x0 % p(3) = a % p(4) = b -% +% +% REFERENCE +% (1) Xia Y, Melinscak F, Bach DR (2020) +% Saccadic Scanpath Length: An Index for Human Threat Conditioning +% Behavioral Research Methods (submitted) %________________________________________________________________________ % PsPM 4.0 diff --git a/src/pspm_cfg/pspm_cfg_data_preprocessing.m b/src/pspm_cfg/pspm_cfg_data_preprocessing.m index c02a4c7d5..f3656005b 100644 --- a/src/pspm_cfg/pspm_cfg_data_preprocessing.m +++ b/src/pspm_cfg/pspm_cfg_data_preprocessing.m @@ -13,6 +13,6 @@ cfg = cfg_repeat; cfg.name = 'Data preprocessing'; cfg.tag = 'data_preprocessing'; -cfg.values = {pspm_cfg_pp_heart_period, pspm_cfg_resp_pp, pspm_cfg_pp_pupil, pspm_cfg_pp_emg}; +cfg.values = {pspm_cfg_pp_heart_period, pspm_cfg_resp_pp, pspm_cfg_pp_pupil, pspm_cfg_pupil_size_convert, pspm_cfg_gaze_convert, pspm_cfg_pp_emg}; cfg.forcestruct = true; cfg.help = {'Help: Data preprocessing'}; \ No newline at end of file diff --git a/src/pspm_cfg/pspm_cfg_gaze_convert.m b/src/pspm_cfg/pspm_cfg_gaze_convert.m new file mode 100644 index 000000000..a900f7dc2 --- /dev/null +++ b/src/pspm_cfg/pspm_cfg_gaze_convert.m @@ -0,0 +1,144 @@ +function [pp_gaze_convert] = pspm_cfg_data_convert +% function [pp_gaze_convert] = pspm_cfg_data_convert(job) +% +% Matlabbatch function for conversion functions of data +%__________________________________________________________________________ +% PsPM 3.1 +% (C) 2016 Tobias Moser (University of Zurich) + +% $Id: pspm_cfg_data_convert.m 635 2019-03-14 10:14:50Z lciernik $ +% $Rev: 635 $ + +% Initialise +global settings +if isempty(settings), pspm_init; end + +%% Datafile +datafile = cfg_files; +datafile.name = 'Data File'; +datafile.tag = 'datafile'; +datafile.num = [1 1]; +datafile.help = {['Specify the PsPM datafile containing the channels ', ... + 'to be converted.'],' ',settings.datafilehelp}; + + +%% width +width = cfg_entry; +width.name = 'Width'; +width.tag = 'width'; +width.strtype = 'r'; +width.num = [1 1]; +width.help = {['Width of the display window. Unit is `mm`.']}; + +%% height +height = cfg_entry; +height.name = 'Height'; +height.tag = 'height'; +height.strtype = 'r'; +height.num = [1 1]; +height.help = {['Height of the display window. Unit is `mm`.']}; + +%% screen distance (Only needed if unit degree is chosen) +screen_distance = cfg_entry; +screen_distance.name = 'Screen distance'; +screen_distance.tag = 'screen_distance'; +screen_distance.strtype = 'r'; +screen_distance.num = [1 1]; +screen_distance.val = {-1}; +screen_distance.help = {['Distance between eye and screen. Unit is `mm` ']}; + + +%% From +from = cfg_menu; +from.name = 'From distance unit'; +from.tag = 'from'; +from.values = { 'pixel', 'mm', 'cm', 'm', 'inches' }; +from.labels = { 'pixel', 'mm', 'cm', 'm', 'inches' }; +from.val = {'mm'}; +from.help = {'Distance unit from which the measurements should be converted.'}; + + +%% Conversions +distance2sps = cfg_branch; +distance2sps.name = 'Distance to scan path speed conversion'; +distance2sps.tag = 'distance2sps'; +distance2sps.val = {width, height, screen_distance, from }; +distance2sps.help = {['Choose conversion information']}; + +distance2degree = cfg_branch; +distance2degree.name = 'Distance to degree conversion'; +distance2degree.tag = 'distance2degree'; +distance2degree.val = {width, height, screen_distance, from }; +distance2degree.help = {['Choose conversion information']}; + +%% Eyes +eyes = cfg_menu; +eyes.name = 'Eyes'; +eyes.tag = 'eyes'; +eyes.val = {'all'}; +eyes.labels = {'All eyes', 'Left eye', 'Right eye'}; +eyes.values = {'lr', 'l', 'r'}; +eyes.help = {['Choose eyes which should be processed. If ''All', ... + 'eyes'' is selected, all eyes which are present in the data will ', ... + 'be processed. Otherwise only the chosen eye will be processed.']}; + +degree2sps = cfg_branch; +degree2sps.name = 'Degree to scan path speed conversion'; +degree2sps.tag = 'degree2sps'; +degree2sps.val = {eyes}; +degree2sps.help = {['Convert degree gaze data to scan path speed.', ... +'This conversion will find the degree unit gaze data from the file automatically.', ... +'The gaze data must not contain any NaN values.']}; + +%% unit +unit = cfg_menu; +unit.name = 'Unit'; +unit.tag = 'unit'; +unit.values = {'mm', 'cm', 'm', 'inches'}; +unit.labels = {'mm', 'cm', 'm', 'inches'}; +unit.val = {'mm'}; +unit.help = {'Unit into which the measurements should be converted.'}; + +%% Channel +channel = cfg_entry; +channel.name = 'Channel'; +channel.tag = 'channel'; +channel.strtype = 'i'; +channel.num = [1 Inf]; +channel.help = {['Specify the channel which should be converted.', ... + 'If 0, conversion will be attmepted on all channels']}; + +pixel2unit = cfg_branch; +pixel2unit.name = 'Pixel to unit'; +pixel2unit.tag = 'pixel2unit'; +pixel2unit.val = {width, height,screen_distance,unit, channel}; +pixel2unit.help = {['Convert pupil gaze coordinates from pixel values ',... + 'to distance unit values']}; + + +%% Conversions +conversion = cfg_choice; +conversion.name = 'Conversion Type'; +conversion.tag = 'conversion'; +conversion.val = {distance2degree}; +conversion.values = {distance2degree, distance2sps, degree2sps,pixel2unit }; +conversion.help = {['']}; + +%% Channel action +chan_action = cfg_menu; +chan_action.name = 'Channel action'; +chan_action.tag = 'channel_action'; +chan_action.val = {'add'}; +chan_action.values = {'replace', 'add'}; +chan_action.labels = {'Replace channel', 'Add channel'}; +chan_action.help = {['Choose whether to ''replace'' the given channel ', ... + 'or ''add'' the converted data as a new channel.']}; + +%% Executable branch +pp_gaze_convert = cfg_exbranch; +pp_gaze_convert.name = 'Gaze convert'; +pp_gaze_convert.tag = 'gaze_convert'; +pp_gaze_convert.val = {datafile, conversion, chan_action}; +pp_gaze_convert.prog = @pspm_cfg_run_gaze_convert; +pp_gaze_convert.help = {['Provides conversion functions for the specified ', ... + 'data (e.g. gaze data).']}; \ No newline at end of file diff --git a/src/pspm_cfg/pspm_cfg_glm_hp_fc.m b/src/pspm_cfg/pspm_cfg_glm_hp_fc.m index b86c18b23..08fc00ce5 100644 --- a/src/pspm_cfg/pspm_cfg_glm_hp_fc.m +++ b/src/pspm_cfg/pspm_cfg_glm_hp_fc.m @@ -33,7 +33,7 @@ soa = cfg_entry; soa.name = 'SOA'; soa.tag = 'soa'; -soa.help = {['Specify custom SOA for response function. Tested values are 3.5s, 4s and 6s. Default: 3.5s']}; +soa.help = {['Specify custom SOA for response function. Tested values are 3.5 s, 4 s and 6 s. Default: 3.5 s']}; soa.strtype = 'r'; soa.num = [1 1]; soa.val = {3.5}; diff --git a/src/pspm_cfg/pspm_cfg_glm_sps.m b/src/pspm_cfg/pspm_cfg_glm_sps.m index 513d3918f..e83180530 100644 --- a/src/pspm_cfg/pspm_cfg_glm_sps.m +++ b/src/pspm_cfg/pspm_cfg_glm_sps.m @@ -30,7 +30,9 @@ soa = cfg_entry; soa.name = 'SOA'; soa.tag = 'soa'; -soa.help = {['Specify custom SOA for response function. Tested values are 3.5s, 4s and 6s. Default: 3.5s']}; +soa.help = {['Specify custom SOA for response function.', ... + 'Tested values are 3.5 s and 4 s.', ... + 'Default: 3.5 s']}; soa.strtype = 'r'; soa.num = [1 1]; soa.val = {3.5}; @@ -39,18 +41,21 @@ % SPS % bf = boxfunction spsrf_box = cfg_const; -spsrf_box.name = 'Boxfunction'; +spsrf_box.name = 'Average scanpath speed'; spsrf_box.tag = 'spsrf_box'; spsrf_box.val = {'spsrf_box'}; -spsrf_box.help = {['SPSRF with boxfunction. (default)']}; +spsrf_box.help = {['This option implements a boxcar function over the SOA, and yields the average ',... + 'scan path speed over that interval (i.e.', ... + 'the scan path length over that interval, divided by the SOA).', ... + '(default).']}; % bf = gammafunction spsrf_gamma = cfg_const; -spsrf_gamma.name = 'Gammafunction'; +spsrf_gamma.name = 'SPSRF_FC'; spsrf_gamma.tag = 'spsrf_gamma'; spsrf_gamma.val = {'spsrf_gamma'}; -spsrf_gamma.help = {['SPSRF with gammafunction.',... - ' Use gamma probability density function to model the scanpath speed. ']}; +spsrf_gamma.help = {['This option implements a gamma function for fear-conditioned scan path speed', ... + 'responses, time-locked to the end of the CS-US interval.']}; rf = cfg_choice; rf.name = 'Function'; @@ -67,3 +72,32 @@ % look for bf and replace b = cellfun(@(f) strcmpi(f.tag, 'bf'), glm_sps.val); glm_sps.val{b} = bf; + +% specific channel +chan_def_left = cfg_const; +chan_def_left.name = 'Last left eye'; +chan_def_left.tag = 'chan_def_left'; +chan_def_left.val = {'pupil_l'}; +chan_def_left.help = {'Use last left eye channel.'}; + +chan_def_right = cfg_const; +chan_def_right.name = 'Last right eye'; +chan_def_right.tag = 'chan_def_right'; +chan_def_right.val = {'pupil_r'}; +chan_def_right.help = {'Use last right eye channel.'}; + +best_eye = cfg_const; +best_eye.name = 'Best eye'; +best_eye.tag = 'best_eye'; +best_eye.val = {'pupil'}; +best_eye.help = {['Use eye with the fewest NaN values.']}; + +chan_def = cfg_choice; +chan_def.name = 'Default'; +chan_def.tag = 'chan_def'; +chan_def.val = {best_eye}; +chan_def.values = {best_eye, chan_def_left, chan_def_right}; + +a = cellfun(@(f) strcmpi(f.tag, 'chan'), glm_sps.val); +glm_sps.val{a}.values{1} = chan_def; +glm_sps.val{a}.val{1} = chan_def; diff --git a/src/pspm_cfg/pspm_cfg_pupil_size_convert.m b/src/pspm_cfg/pspm_cfg_pupil_size_convert.m new file mode 100644 index 000000000..544fd4701 --- /dev/null +++ b/src/pspm_cfg/pspm_cfg_pupil_size_convert.m @@ -0,0 +1,78 @@ +function [pp_pupil_size_convert] = pspm_cfg_pupil_size_convert +% function [pp_pupil_size_convert] = pspm_cfg_pupil_size_convert(job) +% +% Matlabbatch function for pupil size conversion +%__________________________________________________________________________ +% PsPM 4.3 +% (C) 2016 Sam Maxwell (University College London) + + +% Initialise +global settings + +%% Datafile +datafile = cfg_files; +datafile.name = 'Data File'; +datafile.tag = 'datafile'; +datafile.num = [1 1]; +datafile.help = {['Specify the PsPM datafile containing the channels ', ... + 'to be converted.'],' ',settings.datafilehelp}; + +%% Channel +channel = cfg_entry; +channel.name = 'Channel'; +channel.tag = 'channel'; +channel.strtype = 'i'; +channel.num = [1 Inf]; +channel.help = {['Specify the channel which should be converted.', ... + 'If 0, functions are executed on all channels.']}; + +%% area2diameter +area2diameter = cfg_const; +area2diameter.name = 'Area to diameter'; +area2diameter.tag = 'area2diameter'; +area2diameter.val = {'area2diameter'}; +area2diameter.help = {['']}; + +%% Mode +mode = cfg_choice; +mode.name = 'Mode'; +mode.tag = 'mode'; +mode.val = {area2diameter}; +mode.values = {area2diameter}; +mode.help = {['Choose conversion mode.']}; + +%% Conversion +conversion = cfg_branch; +conversion.name = 'Conversion'; +conversion.tag = 'conversion'; +conversion.val = {channel, mode}; +conversion.help = {['']}; + +%% Conversions +conversions = cfg_repeat; +conversions.name = 'Conversion list'; +conversions.tag = 'conversions'; +conversions.values = {conversion}; +conversions.num = [1 Inf]; +conversion.help = {['']}; + +%% Channel action +chan_action = cfg_menu; +chan_action.name = 'Channel action'; +chan_action.tag = 'channel_action'; +chan_action.val = {'add'}; +chan_action.values = {'replace', 'add'}; +chan_action.labels = {'Replace channel', 'Add channel'}; +chan_action.help = {['Choose whether to ''replace'' the given channel ', ... + 'or ''add'' the converted data as a new channel.']}; + +%% Executable branch +pp_pupil_size_convert = cfg_exbranch; +pp_pupil_size_convert.name = 'Pupil size convert'; +pp_pupil_size_convert.tag = 'pupil_size_convert'; +pp_pupil_size_convert.val = {datafile, conversion, chan_action}; +pp_pupil_size_convert.prog = @pspm_cfg_run_pupil_size_convert; +pp_pupil_size_convert.help = {['Provides conversion functions for the specified ', ... + 'data (e.g. pupil size data). Currently only area to diameter conversion is ',... + 'available.']}; \ No newline at end of file diff --git a/src/pspm_cfg/pspm_cfg_run_gaze_convert.m b/src/pspm_cfg/pspm_cfg_run_gaze_convert.m new file mode 100644 index 000000000..13b66c57d --- /dev/null +++ b/src/pspm_cfg/pspm_cfg_run_gaze_convert.m @@ -0,0 +1,25 @@ +function [out] = pspm_cfg_run_gaze_convert(job) + +% $Id$ +% $Rev$ + +channel_action = job.channel_action; +fn = job.datafile{1}; + +options = struct('channel_action', channel_action); +if isfield(job.conversion, 'degree2sps') + % do degree to sps conversion + options.eyes = job.conversion.degree2sps.eyes; + [sts, out] = pspm_convert_visangle2sps(fn, options); +elseif isfield(job.conversion, 'pixel2unit') + args = job.conversion.pixel2unit; + [sts, out] = pspm_convert_pixel2unit(fn, args.channel, args.unit, args.width, args.height, args.screen_distance, options); + +elseif isfield(job.conversion, 'distance2sps') + args = job.conversion.distance2sps; + [sts, out] = pspm_convert_gaze_distance(fn, 'sps', args.from, args.width, args.height, args.screen_distance, options); + +elseif isfield(job.conversion, 'distance2degree') + args = job.conversion.distance2degree; + [ sts, out ] = pspm_convert_gaze_distance(fn, 'degree', args.from, args.width, args.height, args.screen_distance, options); +end diff --git a/src/pspm_cfg/pspm_cfg_run_pupil_size_convert.m b/src/pspm_cfg/pspm_cfg_run_pupil_size_convert.m new file mode 100644 index 000000000..4540c235d --- /dev/null +++ b/src/pspm_cfg/pspm_cfg_run_pupil_size_convert.m @@ -0,0 +1,18 @@ +function [out] = pspm_cfg_run_pupil_size_convert(job) + +% $Id$ +% $Rev$ + +channel_action = job.channel_action; +fn = job.datafile{1}; + +for i=1:numel(job.conversion) + options = struct(); + options.channel_action = channel_action; + chan = job.conversion(i).channel; + if isfield(job.conversion(i).mode, 'area2diameter') + pspm_convert_area2diameter(fn, chan, options); + end +end + +out = 1; diff --git a/src/pspm_compute_visual_angle.m b/src/pspm_compute_visual_angle.m index 3b26448ba..b1552ae24 100644 --- a/src/pspm_compute_visual_angle.m +++ b/src/pspm_compute_visual_angle.m @@ -110,53 +110,18 @@ visual_angl_chans{p} = data{gx}; visual_angl_chans{p+1} = data{gy}; - % get channel specific data - gx_d = data{gx}.data; - gy_d = data{gy}.data; - - % The convention is that the origin of the screen is in the bottom - % left corner, so the following line is not needed a priori, but I - % leave it anyway just in case : - % gy_d = data{gy}.header.range(2)-gy_d; - - N = numel(gx_d); - if N~=numel(gy_d) - warning('ID:invalid_input', 'length of data in gaze_x and gaze_y is not the same'); + try; + [ lat, lon, lat_range, lon_range ] = pspm_compute_visual_angle_core(data{gx}.data, data{gy}.data, width, height, distance, options); + catch; + warning('ID:invalid_input', 'Could not convert distance data to degrees'); return; end; - - % move (0,0) into center of the screen - gx_d = gx_d - width/2; - gy_d = gy_d - height/2; - - % compute visual angle for gaze_x and gaze_y data: - % 1) x axis in cartesian coordinates - s_x = gx_d; - % 2) y axis in cartesian coordinates, actually the distance from participant to the screen - s_y = distance * ones(numel(gx_d),1); - % 3) z axis in spherical coordinates, actually the y axis of the screen - s_z = gy_d; - % 4) convert cartesian to spherical coordinates in radians, - % where azimuth = longitude, elevation = latitude - % the center of spherical coordinates are the eyes of the subject - [azimuth, elevation, ~]= cart2sph(s_x,s_y,s_z); - % 5) convert radians into degrees - lat = rad2deg(elevation); - lon = rad2deg(azimuth); - - % compute visual angle for the range (same procedure) - r_x = [-width/2,width/2,0,0]'; - r_y = distance * ones(numel(r_x),1); - r_z = [0,0,-height/2,height/2]'; - [x_range_sp, y_range_sp,~]= cart2sph(r_x,r_y,r_z); - x_range_sp = rad2deg(x_range_sp); - y_range_sp = rad2deg(y_range_sp); % azimuth angle of gaze points % longitude (azimuth angle from positive x axis in horizontal plane) of gaze points visual_angl_chans{p}.data = lon; visual_angl_chans{p}.header.units = 'degree'; - visual_angl_chans{p}.header.range = [x_range_sp(1),x_range_sp(2)]; + visual_angl_chans{p}.header.range = lon_range % visual_angl_chans{p}.header.r = r; % radial coordinates omitted % visual_angl_chans{p}.header.r_range = r_range; % radial coordinates omitted @@ -164,7 +129,7 @@ % latitude (elevation angle from horizontal plane) of gaze points visual_angl_chans{p+1}.data = lat; visual_angl_chans{p+1}.header.units = 'degree'; - visual_angl_chans{p+1}.header.range = [y_range_sp(3),y_range_sp(4)]; + visual_angl_chans{p+1}.header.range = lat_range % visual_angl_chans{p+1}.header.r = r; % radial coordinates omitted % visual_angl_chans{p+1}.header.r_range = r_range; % radial coordinates omitted diff --git a/src/pspm_compute_visual_angle_core.m b/src/pspm_compute_visual_angle_core.m new file mode 100644 index 000000000..212343742 --- /dev/null +++ b/src/pspm_compute_visual_angle_core.m @@ -0,0 +1,80 @@ +function [lat, lon, lat_range, lon_range] = pspm_compute_visual_angle_core(x_data, y_data, width, height, distance, options) +% pspm_compute_visual_angle computes from gaze data the corresponding +% visual angle (for each data point). The convention used here is that the +% origin of coordinate system for gaze data is at the bottom left corner of +% the screen. +% +% FORMAT: +% [lat, lon, lat_range, lon_range ] = pspm_compute_visual_angle_core(x_data, y_data, options) +% +% ARGUMENTS: +% x_data: X axis data +% y_data: y axis data +% width: screen width in same units as data +% height: screen height in same units as data +% distance: screen distance in same units as data +% options: +% .interpolate: Boolean - Interpolate values +% +% RETURN VALUES +% lat: the latitude in degrees +% lon: the longitude in degrees +% lat_range: the latitude range +% lon_range: the longitude range +%__________________________________________________________________________ +% PsPM 4.0 +global settings; +if isempty(settings), pspm_init; end; + +% interpolate channel specific data if required +if (isfield(options, 'interpolate') && options.interpolate) + interpolate_options = struct('extrapolate', 1); + [ sts_x, gx_d ] = pspm_interpolate(x_data, interpolate_options); + [ sts_x, gy_d ] = pspm_interpolate(y_data, interpolate_options); +else + gx_d = x_data; + gy_d = x_data; +end + +% The convention is that the origin of the screen is in the bottom +% left corner, so the following line is not needed a priori, but I +% leave it anyway just in case : +% gy_d = data{gy}.header.range(2)-gy_d; + +N = numel(gx_d); +if N~=numel(gy_d) + warning('ID:invalid_input', 'length of data in gaze_x and gaze_y is not the same'); + return; +end; + +% move (0,0) into center of the screen +gx_d = gx_d - width/2; +gy_d = gy_d - height/2; + +% compute visual angle for gaze_x and gaze_y data: +% 1) x axis in cartesian coordinates +s_x = gx_d; +% 2) y axis in cartesian coordinates, actually the distance from participant to the screen +s_y = distance * ones(numel(gx_d),1); +% 3) z axis in spherical coordinates, actually the y axis of the screen +s_z = gy_d; +% 4) convert cartesian to spherical coordinates in radians, +% where azimuth = longitude, elevation = latitude +% the center of spherical coordinates are the eyes of the subject +[azimuth, elevation, ~]= cart2sph(s_x,s_y,s_z); +% 5) convert radians into degrees +lat = rad2deg(elevation); +lon = rad2deg(azimuth); + +% compute visual angle for the range (same procedure) +r_x = [-width/2,width/2,0,0]'; +r_y = distance * ones(numel(r_x),1); +r_z = [0,0,-height/2,height/2]'; +[x_range_sp, y_range_sp,~]= cart2sph(r_x,r_y,r_z); + +x_range_sp = rad2deg(x_range_sp); +y_range_sp = rad2deg(y_range_sp); + +lon_range = [x_range_sp(1),x_range_sp(2)]; +lat_range = [y_range_sp(3),y_range_sp(4)]; + diff --git a/src/pspm_convert_gaze_distance.m b/src/pspm_convert_gaze_distance.m new file mode 100644 index 000000000..82811a3f6 --- /dev/null +++ b/src/pspm_convert_gaze_distance.m @@ -0,0 +1,165 @@ +function [sts, out] = pspm_convert_gaze_distance(fn, target, from, width, height, distance, options) +% pspm_convert_gaze_distance takes a file with pixel or length unit gaze data +% and converts to scanpath speed. Data will automatically be interpolated if NaNs exist +% Conversion will be attempted for any gaze data present in the provided unit. +% i.e. if only a left eye's data is provided the speed will only be calculated for that eye. +% +% FORMAT: +% [sts, out] = pspm_convert_gaze_distance(fn, from, width, height, distance, options) +% +% ARGUMENTS: +% fn: The actual data file gaze data +% +% target: target unit of conversion. degree | sps +% +% from: Distance unit to convert from. +% pixel, mm, cm, m, inches +% +% width: Width of the screen in the units chosen in the 'from' parameter +% +% height: Height of the screen in the units chosen in the 'from' parameter +% +% distance: Subject distance from the screen in the units chosen in the 'from' parameter +% +% options: +% channel_action: Channel action for sps data, add / replace existing sps data +% +% OUTPUT: +% sts: Status determining whether the execution was +% successfull (sts == 1) or not (sts == -1) +% out: Output struct +% .channel Id of the added channels. +%__________________________________________________________________________ +% PsPM 4.3.1 +% (C) 2020 Sam Maxwell (University College London) + +% $Id: pspm_convert_gaze_distance.m 1 2020-08-13 12:28:08Z sammaxwellxyz $ +% $Rev: 1 $ + +% initialise +% ----------------------------------------------------------------------------- +global settings; +if isempty(settings), pspm_init; end +sts = -1; + +% Number of arguments validation +if nargin < 6; + warning('ID:invalid_input','Not enough input arguments.'); return; +elseif nargin < 7; + options = struct(); +end + +% Options defaults +if ~isfield(options, 'channel_action'); + options.channel_action = 'add'; +end + + +% Input argument validation +if ~ismember(target, { 'degree', 'sps' }) + warning('ID:invalid_input:target', 'target conversion must be sps or degree'); + return; +end; + +if ~ismember(from, { 'pixel', 'mm', 'cm', 'inches', 'm' }) + warning('ID:invalid_input:from', 'from unit must be "pixel", "mm", "cm", "inches", "m"'); + return; +end; + +if ~isnumeric(height) + warning('ID:invalid_input:height', 'height must be numeric'); + return; +end; + +if ~isnumeric(width) + warning('ID:invalid_input:width', 'width must be numeric'); + return; +end; + +if ~isnumeric(distance) + warning('ID:invalid_input:distance', 'distance must be numeric'); + return; +end; + + +% distance to sps conversion +[sts, infos, data] = pspm_load_data(fn,0); + +eyes.l = find(cellfun(@(c) ~isempty(regexp(c.header.chantype, 'gaze_[x|y]_l', 'once'))... + && strcmp(c.header.units, from), data)); +eyes.r = find(cellfun(@(c) ~isempty(regexp(c.header.chantype, 'gaze_[x|y]_r', 'once'))... + && strcmp(c.header.units, from), data)); + +if (length(eyes.l) < 1 && length(eyes.r) < 1) + warning('ID:invalid_input', 'no gaze data found with the units provided') + return; +end + +for gaze_eye = fieldnames(eyes)' + for d = eyes.(gaze_eye{1})' + sr = data{d}.header.sr; + if ~isempty(regexp(data{d}.header.chantype, 'gaze_x_', 'once')) + lon_chan = data{d}; + + if (strcmp(from, 'pixel')); + data_x = pixel_conversion(data{d}.data, width, data{d}.header.range); + else; + [ sts, data_x ] = pspm_convert_unit(data{d}.data, from, 'mm'); + end; + + else + lat_chan = data{d}; + + if (strcmp(from, 'pixel')); + data_y = pixel_conversion(data{d}.data, height, data{d}.header.range); + else; + [ sts, data_y ] = pspm_convert_unit(data{d}.data, from, 'mm'); + end; + end; + end + + if strcmp(target, 'sps') + options.interpolate = 1; + end + + try; + [ lat, lon, lat_range, lon_range ] = pspm_compute_visual_angle_core(data_x, data_y, width, height, distance, options); + catch; + warning('ID:invalid_input', 'Could not convert distance data to degrees'); + return; + end; + + + if strcmp(target, 'degree') + lat_chan.data = lat; + lat_chan.header.units = 'degree'; + lat_chan.header.range = lat_range; + + lon_chan.data = lon; + lon_chan.header.units = 'degree'; + lon_chan.header.range = lon_chan; + + [sts, out] = pspm_write_channel(fn, { lat_chan, lon_chan }, options.channel_action); + elseif strcmp(target, 'sps') + + arclen = pspm_convert_visangle2sps_core(lat, lon); + dist_channel.data = rad2deg(arclen) .* sr; + dist_channel.header.chantype = strcat('sps_', gaze_eye{1}); + dist_channel.header.sr = sr; + dist_channel.header.units = 'degree'; + + [sts, out] = pspm_write_channel(fn, dist_channel, options.channel_action); + end + +end +end + + +% CODE SAME AS IN pspm_pixel2unit +function out = pixel_conversion(data, screen_length, interest_range) + length_per_pixel = screen_length ./ (diff(interest_range) + 1); + % baseline data in pixels wrt. the range (i.e. pixels of interest) + pixel_index = data-interest_range(1); + % convert indices into coordinates in the units of interests + out = pixel_index * length_per_pixel; +end diff --git a/src/pspm_convert_pixel2unit.m b/src/pspm_convert_pixel2unit.m index 74d97072b..85e3316c7 100644 --- a/src/pspm_convert_pixel2unit.m +++ b/src/pspm_convert_pixel2unit.m @@ -14,7 +14,7 @@ % fn: File to convert. % chan: On which subset of channels should the conversion % be done. Supports all values which can be passed -% to pspm_load_data(). The will only work on +% to pspm_load_data(). This will only work on % gaze-channels. Other channels specified will be % ignored.(For conversion into 'degree' there must be % the same amount of gane_x as gaze_y channels) @@ -22,7 +22,7 @@ % converted. % The value can contain any length unit or % 'degree'. In this case the corresponding data -% is firstly convertet into 'mm' and +% is firstly converted into 'mm' and % afterwards the visual angles are computed. % width: Width of the display window. Unit is 'mm' % if 'degree' is chosen, otherwise 'unit'. diff --git a/src/pspm_convert_visangle2sps.m b/src/pspm_convert_visangle2sps.m index 73ccaefb8..f87d93032 100644 --- a/src/pspm_convert_visangle2sps.m +++ b/src/pspm_convert_visangle2sps.m @@ -1,27 +1,30 @@ function [ sts, out ] = pspm_convert_visangle2sps(fn, options) -% pspm_convert_visangle2sp_speed takes a file with data from eyelink recordings -% and computes by time units normalized distances bewteen visual angle data. +% pspm_convert_visangle2sps takes a file with data from eyelink recordings +% and computes by seconds normalized distances bewteen visual angle data. % It saves the result into a new channel with chaneltype 'sps' (Scanpath speed). % It is important that pspm_convert_visangle2sps only takes channels % which are in visual angle. % FORMAT: -% [ sts, out ] = pspm_convert_visangle2sp_speed(fn, options) +% [ sts, out ] = pspm_convert_visangle2sps(fn, options) % ARGUMENTS: -% fn: The actual data file containing the eyelink -% recording with gaze data +% fn: The actual data file containing the eyelink +% recording with gaze data % options. -% chans: On which subset of the channels the visual -% angles between the data point should be -% computed . -% If no channels are given then the function -% computes the scanpath speed of the first -% found gaze data channels with type 'degree' -% eyes: Define on which eye the operations -% should be performed. Possible values -% are: 'l', 'r', 'lr', 'rl'. -% Default: 'lr' -% +% chans: On which subset of the channels the visual +% angles between the data point should be +% computed . +% If no channels are given then the function +% computes the scanpath speed of the first +% found gaze data channels with type 'degree' +% eyes: Define on which eye the operations +% should be performed. Possible values +% are: 'l', 'r', 'lr', 'rl'. +% Default: 'lr' +% .channel_action: ['add'/'replace'] Defines whether the new channels +% should be added or the previous outputs of this function +% should be replaced. +% Default: 'add' % OUTPUT: % sts: Status determining whether the execution was % successfull (sts == 1) or not (sts == -1) @@ -38,7 +41,6 @@ warning('ID:invalid_input', 'Nothing to do.'); return; elseif nargin<2 channels = 0; - options = struct('eyes','lr'); end if isfield(options, 'chans') channels = options.chans; @@ -58,6 +60,13 @@ '''r'', ''rl'' or ''lr''.']); return; end; +% option.channel_action +if ~isfield(options, 'channel_action') + options.channel_action = 'add'; +elseif ~any(strcmpi(options.channel_action, {'add', 'replace'})) + warning('ID:invalid_input', ['''options.channel_action'' must be either ''add'' or ''replace''.']); + return; +end; % fn if ~ischar(fn) || ~exist(fn, 'file') @@ -91,49 +100,23 @@ % get channel specific data lon = data{gx}.data; lat = data{gy}.data; -% lat = data{gy}.header.range(2)-lat; - - % first interpolate longitude to evict NaN-values - [bsts,outdata]=pspm_interpolate(lon); - if bsts ~= 1 - warning('ID:invalid_input', 'Could not load interpolate longitude data correctly.'); - return; - end - lon = outdata; - - % first interpolate latitude to evict NaN-values - [bsts,outdata]=pspm_interpolate(lat); - if bsts ~= 1 - warning('ID:invalid_input', 'Could not load interpolate latitude data correctly.'); - return; - end - lat = outdata; - - %compare if length are the same - N = numel(lon); - if N~=numel(lat) - warning('ID:invalid_input', 'length of data in gaze_x and gaze_y is not the same'); - return; - end; - - %convert lon and lat into radians - lon = deg2rad(lon); - lat = deg2rad(lat); - % compute distances - arclen = zeros(length(lat),1); - for k = 2:length(lat) - lon_diff = abs(lon(k-1)-lon(k)); - arclen(k) = atan(sqrt(((cos(lat(k))*sin(lon_diff))^2)+(((cos(lat(k-1))*sin(lat(k)))-(sin(lat(k-1))*cos(lat(k))*cos(lon_diff)))^2))/((sin(lat(k-1))*sin(lat(k)))+(cos(lat(k-1))*cos(lat(k))*cos(lon_diff)))); + try + arclen = pspm_convert_visangle2sps_core(lat, lon); + catch + warning('ID:invalid_input', 'Could not calculate sps from gaze data'); + return end + + % create new channel with data holding distances - dist_channel.data = rad2deg(arclen); - dist_channel.header.chantype = 'sps'; + dist_channel.data = rad2deg(arclen) .* data{gx}.header.sr; + dist_channel.header.chantype = strcat('sps_', eye); dist_channel.header.sr = data{gx}.header.sr; dist_channel.header.units = 'degree'; - - - [lsts, outinfo] = pspm_write_channel(fn, dist_channel, 'add'); + + + [lsts, outinfo] = pspm_write_channel(fn, dist_channel, options.channel_action); if lsts ~= 1 warning('ID:invalid_input', '~Distance channel could not be written'); @@ -143,8 +126,9 @@ out(i) = outinfo; else + if strcmpi(eye,'r'), eye_long='right'; else, eye_long='left'; end warning('ID:invalid_input', ['Unable to perform visangle2', ... - 'sps. Cannot find gaze channels with degree ',... + 'sps for the ',eye_long,' eye. Cannot find gaze channels with degree ',... 'unit values. Maybe you need to convert them with ', ... 'pspm_convert_pixel2unit()']); end; diff --git a/src/pspm_convert_visangle2sps_core.m b/src/pspm_convert_visangle2sps_core.m new file mode 100644 index 000000000..2c86f44ba --- /dev/null +++ b/src/pspm_convert_visangle2sps_core.m @@ -0,0 +1,27 @@ +function arclen = pspm_convert_visangle2sps_core(lat, lon) + +if sum(isnan(lon)) > 0 || sum(isnan(lat)) > 0 + warning('ID:invalid_input', 'cannot calculate sps from data with NaN values, check gaze degree data for NaNs'); + return; +end + +%compare if length are the same +if numel(lon) ~=numel(lat) + warning('ID:invalid_input', 'length of data in gaze_x and gaze_y is not the same'); + return; +end; + + +%convert lon and lat into radians +lon = deg2rad(lon); +lat = deg2rad(lat); +% compute distances +arclen = zeros(length(lat),1); + +% Haversine +lat_diff = (lat(2:end) - lat(1:end - 1)) / 2; +lon_diff = (lon(2:end) - lon(1:end - 1)) / 2; + +theta = sin(lat_diff).^2 + cos(lat(1:end - 1)) .* cos(lat(2:end)) .* sin(lon_diff).^2; + +arclen(2:end) = 2 * atan2(sqrt(theta),sqrt(1 - theta)); \ No newline at end of file diff --git a/src/pspm_get_sp_speed.m b/src/pspm_get_sps.m similarity index 59% rename from src/pspm_get_sp_speed.m rename to src/pspm_get_sps.m index 8e6557b2f..992c60096 100644 --- a/src/pspm_get_sp_speed.m +++ b/src/pspm_get_sps.m @@ -1,9 +1,9 @@ -function [sts, data] = pspm_get_sp_speed(import ) -% pspm_get_sp_speed is a comon function for importing eyelink data (distances +function [sts, data] = pspm_get_sps(import) +% pspm_get_sps is a comon function for importing eyelink data (distances % between following data points) % % FORMAT: -% [sts, data]=pspm_get_sp_speed(import) +% [sts, data]=pspm_get_sps(import) % with import.data: column vector of waveform data % import.sr: sample rate % @@ -11,14 +11,16 @@ global settings; if isempty(settings), pspm_init; end; + + % initialise status sts = -1; -% assign respiratory data +% assign sps data data.data = import.data(:); % add header -data.header.chantype = 'sp_speed'; +data.header.chantype = 'sps'; data.header.units = import.units; data.header.sr = import.sr; data.header.range = import.range; diff --git a/src/pspm_get_sps_l.m b/src/pspm_get_sps_l.m new file mode 100644 index 000000000..54f53edee --- /dev/null +++ b/src/pspm_get_sps_l.m @@ -0,0 +1,32 @@ +function [sts, data] = pspm_get_sps_l(import) +% pspm_get_sps_l is a comon function for importing left eye eyelink data (distances +% between following data points) +% +% FORMAT: +% [sts, data]=pspm_get_sps_l(import) +% with import.data: column vector of waveform data +% import.sr: sample rate +% + +global settings; +if isempty(settings), pspm_init; end; + + + +% initialise status +sts = -1; + +% assign sps data +data.data = import.data(:); + +% add header +data.header.chantype = 'sps_l' +data.header.units = import.units; +data.header.sr = import.sr; +data.header.range = import.range; + +% check status +sts = 1; + +end + diff --git a/src/pspm_get_sps_r.m b/src/pspm_get_sps_r.m new file mode 100644 index 000000000..e9084c5bd --- /dev/null +++ b/src/pspm_get_sps_r.m @@ -0,0 +1,32 @@ +function [sts, data] = pspm_get_sps_r(import) +% pspm_get_sps_r is a comon function for importing right eye eyelink data (distances +% between following data points) +% +% FORMAT: +% [sts, data]=pspm_get_sps_r(import) +% with import.data: column vector of waveform data +% import.sr: sample rate +% + +global settings; +if isempty(settings), pspm_init; end; + + + +% initialise status +sts = -1; + +% assign sps data +data.data = import.data(:); + +% add header +data.header.chantype = 'sps_r' +data.header.units = import.units; +data.header.sr = import.sr; +data.header.range = import.range; + +% check status +sts = 1; + +end + diff --git a/src/pspm_glm.m b/src/pspm_glm.m index c49aa9ac7..e7f5d10b2 100644 --- a/src/pspm_glm.m +++ b/src/pspm_glm.m @@ -68,6 +68,10 @@ % at the end of the output. In 'free' models the field % model.window is MANDATORY and single basis functions % are allowed only. +% model.centering: if set to 0 the function would not perform the +% mean centering of the convolved X data. For example, to +% invert SPS model, set centering to 0. +% Default: 1 % % OPTIONS (optional argument) % options.overwrite: overwrite existing model output; default 0 diff --git a/src/pspm_init.m b/src/pspm_init.m index ef371a1e1..f0a6ab6f8 100644 --- a/src/pspm_init.m +++ b/src/pspm_init.m @@ -373,27 +373,39 @@ defaults.chantypes(30) = ... struct('type', 'sps', ... 'description', 'Scanpath speed', ... - 'import', @pspm_get_sp_speed, ... + 'import', @pspm_get_sps, ... 'data', 'wave'); -% documented defaults.chantypes(31) = ... + struct('type', 'sps_r', ... + 'description', 'Scanpath speed', ... + 'import', @pspm_get_sps_r, ... + 'data', 'wave'); + +defaults.chantypes(32) = ... + struct('type', 'sps_l', ... + 'description', 'Scanpath speed', ... + 'import', @pspm_get_sps_l, ... + 'data', 'wave'); + +% documented +defaults.chantypes(33) = ... struct('type', 'custom', ... 'description', 'Custom', ... 'import', @pspm_get_custom, ... 'data', 'wave'); -defaults.chantypes(32) = ... +defaults.chantypes(34) = ... struct('type', 'pupil_r_pp', ... 'description', 'Pupil right preprocessed', ... 'import', @none, ... 'data', 'wave'); % documented -defaults.chantypes(33) = ... +defaults.chantypes(35) = ... struct('type', 'pupil_l_pp', ... 'description', 'Pupil left preprocessed', ... 'import', @none, ... 'data', 'wave'); -defaults.chantypes(34) = ... +defaults.chantypes(36) = ... struct('type', 'pupil_lr_pp', ... 'description', 'Pupil combined preprocessed', ... 'import', @none, ... diff --git a/src/pspm_write_channel.m b/src/pspm_write_channel.m index 8b7015d18..54895abd4 100644 --- a/src/pspm_write_channel.m +++ b/src/pspm_write_channel.m @@ -124,6 +124,7 @@ % ------------------------------------------------------------------------- [nsts, infos, data] = pspm_load_data(fn); if nsts == -1, return; end +importdata = data; %% Find channel according to action % ------------------------------------------------------------------------- @@ -139,8 +140,9 @@ channeli = find(strcmpi(options.channel,fchannels),1,options.delete); end elseif options.channel == 0 - channeli = cellfun(@(x) find(strcmpi(x.header.chantype, fchannels),1,'last'), ... - newdata, 'UniformOutput', 0); + funits = cellfun(@(x) x.header.units, data,'UniformOutput',0); + % if the chantype matches, and unit matches if one is provided + channeli = cellfun(@(n) match_channel(fchannels, funits, n), newdata, 'UniformOutput', 0); channeli = cell2mat(channeli); else channel = options.channel; @@ -228,3 +230,14 @@ sts = 1; end + + +function matches = match_channel(existing_channels, exisiting_units, channel) + if isfield(channel.header, 'units') + matches = find(... + strcmpi(channel.header.chantype, existing_channels)... + & strcmpi(channel.header.units, exisiting_units),1,'last'); + else + matches = find(strcmpi(channel.header.chantype, existing_channels) ,1,'last'); + end +end \ No newline at end of file diff --git a/test/pspm_convert_gaze_distance_test.m b/test/pspm_convert_gaze_distance_test.m new file mode 100644 index 000000000..4d8e26e64 --- /dev/null +++ b/test/pspm_convert_gaze_distance_test.m @@ -0,0 +1,116 @@ +classdef pspm_convert_gaze_distance_test < matlab.unittest.TestCase +% pspm_convert_gaze_distance_test +% unittest class for the pspm_convert_gaze_distance_test function + + + properties + raw_input_filename = fullfile('ImportTestData', 'eyelink', 'S114_s2.asc'); + fn = ''; + end + + properties (TestParameter) + channel_action = { 'add', 'replace' }; + from = { 'pixel', 'mm', 'inches' }; + target = { 'degree', 'sps' }; + end + + methods + % get the gaze data channels for a specific unit + function len = get_gaze_and_unit(this, data, unit) + len = find(cellfun(@(c) strcmp(c.header.units, unit) && ~isempty(regexp(c.header.chantype, 'gaze_[x|y]_[r|l]')), data)); + end; + end + + methods(TestMethodSetup) + function backup(this) + import = {}; + import{end + 1}.type = 'pupil_r'; + import{end}.eyelink_trackdist = 600; + import{end}.distance_unit = 'mm'; + import{end + 1}.type = 'pupil_l'; + import{end}.eyelink_trackdist = 600; + import{end}.distance_unit = 'mm'; + import{end + 1}.type = 'gaze_x_r'; + import{end + 1}.type = 'gaze_y_r'; + import{end + 1}.type = 'gaze_x_l'; + import{end + 1}.type = 'gaze_y_l'; + import{end + 1}.type = 'marker'; + options.overwrite = true; + this.fn = pspm_import(this.raw_input_filename, 'eyelink', import, options); + this.fn = this.fn{1}; + end + end + + + methods (Test) + + function validations(this, target) + this.verifyWarningFree(@() pspm_convert_gaze_distance(this.fn, target, 'pixel', 111, 222, 333)); + this.verifyWarning(@() pspm_convert_gaze_distance(this.fn, target, "not_a_unit", 111, 222, 333), 'ID:invalid_input:from'); + this.verifyWarning(@() pspm_convert_gaze_distance(this.fn, target, "pixel", 'not_a_number', 222, 333), 'ID:invalid_input:width'); + this.verifyWarning(@() pspm_convert_gaze_distance(this.fn, target, "pixel", 111, 'not_a_number', 333), 'ID:invalid_input:height'); + this.verifyWarning(@() pspm_convert_gaze_distance(this.fn, target, "pixel", 111, 222, 'not_a_number'), 'ID:invalid_input:distance'); + this.verifyWarning(@() pspm_convert_gaze_distance(this.fn, 'invalid_conversion', 'pixel', 111, 222, 333), 'ID:invalid_input:target'); + end + + + function conversion(this, target, from, channel_action) + load(this.fn); + width = 323; + height = 232; + distance = 600; + + if (~strcmp(from, 'pixel')); + pspm_convert_pixel2unit(this.fn, 0, from, width, height, distance); + load(this.fn); + this.verifyLength(this.get_gaze_and_unit(data, from), 4); + end; + + data_length = length(data); + if strcmp(target, 'degree') + this.verifyLength(this.get_gaze_and_unit(data, 'degree'), 0); + else + this.verifyLength(find(cellfun(@(c) strcmp(c.header.chantype, 'sps_l'), data)), 0); + this.verifyLength(find(cellfun(@(c) strcmp(c.header.chantype, 'sps_r'), data)), 0); + end + + [sts, out_channel] = this.verifyWarningFree(@() pspm_convert_gaze_distance(... + this.fn, target, from, width, height, distance, struct('channel_action', channel_action))); + load(this.fn); + this.verifyTrue(length(out_channel.channel) > 0); + + extra = 2; + if strcmp(target, 'degree') + extra = 4; + end + + this.verifyLength(data, data_length + extra); + data_length = length(data); + + if strcmp(target, 'degree') + this.verifyLength(this.get_gaze_and_unit(data, 'degree'), 4); + else + this.verifyLength(find(cellfun(@(c) strcmp(c.header.chantype, 'sps_l'), data)), 1); + this.verifyLength(find(cellfun(@(c) strcmp(c.header.chantype, 'sps_r'), data)), 1); + end + + [sts, out_channel] = this.verifyWarningFree(@() pspm_convert_gaze_distance(... + this.fn, target, from, width, height, distance, struct('channel_action', channel_action))); + load(this.fn); + + extra = 0; + if (strcmp(channel_action, 'add')); + if strcmp(target, 'degree') + extra = extra + 4; + else + extra = extra + 2; + end + end; + + this.verifyLength(data, data_length + extra); + this.verifyEqual(sts, 1); + + end + end + +end \ No newline at end of file diff --git a/test/pspm_get_sps_test.m b/test/pspm_get_sps_test.m new file mode 100644 index 000000000..c6bb26b17 --- /dev/null +++ b/test/pspm_get_sps_test.m @@ -0,0 +1,33 @@ +classdef pspm_get_sps_test < matlab.unittest.TestCase +% SCR_GET_SPIKE_TEST +% unittest class for the pspm_get_sps_test function +%__________________________________________________________________________ +% SCRalyze TestEnvironment +% (C) 2013 Linus R�ttimann (University of Zurich) + + methods (Test) + function invalid_eye(this) + + import.sr = 100; + import.data = ones(1,1000); + import.units = 'degree'; + import.range = [ 0, 1]; + + [ sts, out ] = this.verifyWarningFree(@() pspm_get_sps(import)); + this.verifyEqual(sts, 1); + this.verifyEqual(out.header.chantype, 'sps'); + + [ sts, out ] = this.verifyWarningFree(@() pspm_get_sps_l(import)); + this.verifyEqual(sts, 1); + this.verifyEqual(out.header.chantype, 'sps_l'); + + [ sts, out ] = this.verifyWarningFree(@() pspm_get_sps_r(import)); + this.verifyEqual(sts, 1); + this.verifyEqual(out.header.chantype, 'sps_r'); + + end + + end + +end + diff --git a/test/pspm_test.m b/test/pspm_test.m index d09bc9d27..0de2cc015 100644 --- a/test/pspm_test.m +++ b/test/pspm_test.m @@ -54,6 +54,7 @@ function pspm_test(varargin) TestSuite.fromClass(?pspm_pupil_pp_test), ... TestSuite.fromClass(?set_blinks_saccades_to_nan_test), ... TestSuite.fromClass(?pspm_blink_saccade_filt_test), ... + TestSuite.fromClass(?pspm_convert_gaze_distance_test), ... ]; @@ -92,6 +93,7 @@ function pspm_test(varargin) TestSuite.fromClass(?pspm_get_pupil_test), ... TestSuite.fromClass(?pspm_get_resp_test), ... TestSuite.fromClass(?pspm_get_scr_test), ... + TestSuite.fromClass(?pspm_get_sps_test), ... ]; full_suite = [suite, import_suite, chantype_suite]; diff --git a/test/pspm_testdata_gen.m b/test/pspm_testdata_gen.m index e392f6b15..48ff00235 100755 --- a/test/pspm_testdata_gen.m +++ b/test/pspm_testdata_gen.m @@ -15,6 +15,7 @@ % - .chantype: 'scr', 'hr', 'hb', 'resp', 'trigger', 'scanner' % and optional fields: % if .chantype is 'scr' or 'hr' (continuous channels): +% - .units: units to write to channel, defaults to 'unknown' for continuous and 'events' for event data % - .sr: sampling rate for waveform (default value is 100Hz) % - .freq: frequencey of the waveform (default value is 1Hz) % - .noise: (default: 0) if 1 will add random normally @@ -90,6 +91,9 @@ cont_channels{3} = 'resp'; cont_channels{4} = 'snd'; +% regex expression for scr OR hr OR resp OR snd OR gaze with x/y and r/l +cont_channels_regex = '^(scr|hr|resp|snd|gaze_[x|y]_[r|l])$'; + % Eventbased Channels event_channels{1} = 'hb'; event_channels{2} = 'rs'; @@ -112,7 +116,7 @@ if ~isfield(channels{k}, 'chantype') warning('No type given for channels job %2.0f', k); outfile = cell(0); return; - elseif any(strcmp(channels{k}.chantype, cont_channels)) + elseif regexp(channels{k}.chantype, cont_channels_regex) %default values if ~isfield(channels{k}, 'freq') channels{k}.freq = 1; @@ -121,7 +125,13 @@ channels{k}.noise = 0; end outfile.data{k,1}.header = channels{k}; - outfile.data{k,1}.header.units = 'unknown'; + + if isfield(channels{k}, 'units') + outfile.data{k,1}.header.units = channels{k}.units; + else + outfile.data{k,1}.header.units = 'unknown'; + end + outfile.data{k,1}.header.chantype = channels{k}.chantype; %Generate sinewaveform @@ -188,7 +198,13 @@ end outfile.data{k,1}.header = channels{k}; - outfile.data{k,1}.header.units = 'events'; + + if isfield(channels{k}, 'units') + outfile.data{k,1}.header.units = channels{k}.units; + else + outfile.data{k,1}.header.units = 'events'; + end + outfile.data{k,1}.header.chantype = channels{k}.chantype; outfile.data{k,1}.data = ev_data; diff --git a/test/pspm_write_channel_test.m b/test/pspm_write_channel_test.m index b018579ec..cb45808c8 100644 --- a/test/pspm_write_channel_test.m +++ b/test/pspm_write_channel_test.m @@ -26,6 +26,8 @@ function generate_testdatafile(this) c{1}.chantype = 'scr'; c{2}.chantype = 'marker'; c{3}.chantype = 'scr'; + c{4}.chantype = 'gaze_x_l'; + c{4}.units = 'mm'; data_settings.channels = c; data_settings.sr = 100; data_settings.duration = 500; @@ -208,6 +210,39 @@ function test_replace(this) this.verifyWarningFree(@() this.verify_write(new, old, gen_data, 'replace', outinfos)); end; + function test_replace_units(this) + % change channel setting and regenerate data + c{1}.chantype = 'gaze_x_l'; + c{1}.units = 'mm'; + c{1}.sr = 20; + gen_data = pspm_testdata_gen(c, 500); + + % load file before + [~, old.infos, old.data] = pspm_load_data(this.testdatafile); + + % channel should exist -> should actually replace + [~, outinfos] = this.verifyWarningFree(@() pspm_write_channel(this.testdatafile, gen_data.data{1}, 'replace')); + + % load changed data + [~, new.infos, new.data] = pspm_load_data(this.testdatafile); + + % check if has been replaced + this.verifyWarningFree(@() this.verify_write(new, old, gen_data, 'replace', outinfos)); + + % change units + gen_data.data{1}.header.units = 'degree'; + [~, outinfos] = this.verifyWarningFree(@() pspm_write_channel(this.testdatafile, gen_data.data{1}, 'replace')); + [~, post_unit_change.infos, post_unit_change.data] = pspm_load_data(this.testdatafile); + + % should be one more channel as degrees did not exist + this.verifyEqual(length(post_unit_change.data), length(new.data) + 1); + + % assert one mm gaze channel and one degree gaze channel + this.verifyEqual(length(find(cellfun(@(c) strcmp(c.header.units, 'mm') && strcmp(c.header.chantype, 'gaze_x_l'), post_unit_change.data))), 1); + this.verifyEqual(length(find(cellfun(@(c) strcmp(c.header.units, 'degree') && strcmp(c.header.chantype, 'gaze_x_l'), post_unit_change.data))), 1); + + end; + function test_delete_single(this) %% Delete with chantype From 09f27e5a9713725eb79b3e5452846348bc80c69f Mon Sep 17 00:00:00 2001 From: Sam Maxwell Date: Mon, 24 Aug 2020 15:05:07 +0100 Subject: [PATCH 15/31] Merge pull request #149 from bachlab/feat/events_flank_gui Add flank option in the GUI import module --- src/pspm_cfg/pspm_cfg_import.m | 22 +++++++++++++++++++++- src/pspm_cfg/pspm_cfg_run_import.m | 8 ++++++++ src/pspm_get_events.m | 2 +- src/pspm_get_eyelink.m | 6 ++++-- src/pspm_get_smi.m | 5 ++++- src/pspm_get_viewpoint.m | 5 ++++- 6 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/pspm_cfg/pspm_cfg_import.m b/src/pspm_cfg/pspm_cfg_import.m index 5d8b6c9fa..0b95895e8 100644 --- a/src/pspm_cfg/pspm_cfg_import.m +++ b/src/pspm_cfg/pspm_cfg_import.m @@ -10,6 +10,7 @@ % Get filetype fileoptions={settings.import.datatypes.long}; chantypesDescription = {settings.chantypes.description}; +chantypesData = {settings.chantypes.data}; %% Predefined struct @@ -223,6 +224,20 @@ 'this channel by its name. Note: the channel number refers to the n-th recorded ' ... 'channel, not to its number during acquisition (if you did not save all recorded ' ... 'channels, these might be different for some data types).']}; + + %% Flank option for 'event' channel types + flank_option = cfg_menu; + flank_option.name = 'Flank of the event impulses to import'; + flank_option.tag = 'flank_option'; + flank_option.values = {'ascending', 'descending', 'all', 'both', 'default'}; + flank_option.labels = {'ascending', 'descending', 'both', 'middle', 'default'}; + flank_option.val = {'default'}; + flank_option.help = {['The flank option specifies which of the rising edge(ascending), ', ... + 'falling edge(descending), both edges or their mean(middle) of a marker impulse should ', ... + 'be imported into the marker channel. The default option is to select the middle of ', ... + 'the impulse, some exceptions are Eyelink, ViewPoint and SensoMotoric Instruments data ', ... + 'for which the default are respectively ''both'', ''ascending'', ''ascending''. ',... + 'If the numbers of rising and falling edges differ, PsPM will throw an error. ']}; %% Channel/Column Type Items importtype_item = cell(1,length(chantypes)); @@ -268,7 +283,12 @@ end end - importtype_item{importtype_i}.val = {chan_nr}; + if strcmp(chantypesData{chantypesDescIdx}, 'events') + importtype_item{importtype_i}.val = {chan_nr,flank_option}; + else + importtype_item{importtype_i}.val = {chan_nr}; + end + % Check for sample rate if samplerate == 0 diff --git a/src/pspm_cfg/pspm_cfg_run_import.m b/src/pspm_cfg/pspm_cfg_run_import.m index 7d4397fc3..14ab4e60b 100644 --- a/src/pspm_cfg/pspm_cfg_run_import.m +++ b/src/pspm_cfg/pspm_cfg_run_import.m @@ -42,6 +42,14 @@ import{i}.sr = job.datatype.(datatype).importtype{i}.(type{1}).sample_rate; end + % Check if flank option is available + if isfield(job.datatype.(datatype).importtype{i}.(type{1}), 'flank_option') && ... + ~strcmp(job.datatype.(datatype).importtype{i}.(type{1}).flank_option, 'default') + + import{i}.flank = job.datatype.(datatype).importtype{i}.(type{1}).flank_option; + + end + % Check if transfer function available if isfield(job.datatype.(datatype).importtype{i}.(type{1}), 'scr_transfer') transfer = fieldnames(job.datatype.(datatype).importtype{i}.(type{1}).scr_transfer); diff --git a/src/pspm_get_events.m b/src/pspm_get_events.m index 8d09be9c5..f29a8aa96 100644 --- a/src/pspm_get_events.m +++ b/src/pspm_get_events.m @@ -133,7 +133,7 @@ elseif isfield(import, 'markerinfo') && ... (numel(data) - numel(import.markerinfo.value))/import.sr < 1 % also translate marker info if necessary. this code was written - % with and for import_eyelink function. there flank = 'ascending' + % with and for import_eyelink function. there flank = 'all' % has to be set to use import.data as index for the marker values. n_minfo = struct('value', {import.markerinfo.value(round(import.data*import.sr))}, ... diff --git a/src/pspm_get_eyelink.m b/src/pspm_get_eyelink.m index a3c1e37dc..49c89f34e 100644 --- a/src/pspm_get_eyelink.m +++ b/src/pspm_get_eyelink.m @@ -265,8 +265,10 @@ % imported data cannot be read at the moment (in later instances) import{k}.markerinfo = markerinfos; - % use 'all' flank for translation from continuous to events - import{k}.flank = 'all'; + % by default use 'all' flank for translation from continuous to events + if ~isfield(import{k},'flank') + import{k}.flank = 'all'; + end else % determine chan id from chantype - eyelink specific % thats why channel ids will be ignored! diff --git a/src/pspm_get_smi.m b/src/pspm_get_smi.m index 217dc758c..cd0a04853 100644 --- a/src/pspm_get_smi.m +++ b/src/pspm_get_smi.m @@ -332,7 +332,10 @@ function [import_cell, chan_id] = import_marker_chan(import_cell, markers, mi_values, mi_names, n_rows, sampling_rate) import_cell.marker = 'continuous'; - import_cell.flank = 'ascending'; + % by default use 'ascending' flank for SMI data + if ~isfield(import_cell,'flank') + import_cell.flank = 'ascending'; + end import_cell.sr = sampling_rate; import_cell.data = false(n_rows, 1); marker_indices = 1 + markers * sampling_rate; diff --git a/src/pspm_get_viewpoint.m b/src/pspm_get_viewpoint.m index c30ff14ba..76085702b 100644 --- a/src/pspm_get_viewpoint.m +++ b/src/pspm_get_viewpoint.m @@ -323,7 +323,10 @@ mi_values = mi_values(non_empty,1); import_cell.marker = 'continuous'; - import_cell.flank = 'ascending'; + % by default use 'ascending' flank for ViewPoint data + if ~isfield(import_cell,'flank') + import_cell.flank = 'ascending'; + end import_cell.sr = sampling_rate; import_cell.data = false(n_rows, 1); marker_indices = 1 + markers * sampling_rate; From 26750c2edf4f51579fb74cb352e6a42467825235 Mon Sep 17 00:00:00 2001 From: Sam Maxwell Date: Tue, 25 Aug 2020 17:02:26 +0100 Subject: [PATCH 16/31] Merge pull request #147 from bachlab/release/4.4 release(4.4.0): update release notes frst pass --- doc/PsPM_Developers_Guide.lyx | 2 +- doc/PsPM_Manual.lyx | 155 +++++++++++++++++++++++++++++++++- doc/PsPM_release_checklist.md | 2 +- doc/release_notes.tex | 46 ++++++++++ src/pspm.fig | Bin 25494 -> 25489 bytes src/pspm_msg.txt | 4 +- src/pspm_quit.m | 2 +- test/pspm_pp_test.m | 2 +- 8 files changed, 204 insertions(+), 9 deletions(-) diff --git a/doc/PsPM_Developers_Guide.lyx b/doc/PsPM_Developers_Guide.lyx index a3ced3e5d..15dfc4731 100644 --- a/doc/PsPM_Developers_Guide.lyx +++ b/doc/PsPM_Developers_Guide.lyx @@ -98,7 +98,7 @@ Developer's Guide \begin_layout Standard \align center -Version 4.3.0 +Version 4.4.0 \end_layout \begin_layout Standard diff --git a/doc/PsPM_Manual.lyx b/doc/PsPM_Manual.lyx index 90e1a6815..b08ce7d3d 100644 --- a/doc/PsPM_Manual.lyx +++ b/doc/PsPM_Manual.lyx @@ -103,7 +103,7 @@ PsPM: Psychophysiological Modelling \begin_layout Standard \align center -Version 4.3.0 +Version 4.4.0 \end_layout \begin_layout Standard @@ -150,8 +150,8 @@ literal "false" \size larger Dominik R Bach, Giuseppe Castegnetti, Laure Ciernik, Samuel Gerster, Saurabh - Khemka, Christoph Korn, Tobias Moser, Philipp C Paulus, Ivan Rojkov, Matthias - Staib, Eshref Yozdemir and collaborators + Khemka, Christoph Korn, Samuel Maxwell, Tobias Moser, Philipp C Paulus, + Ivan Rojkov, Matthias Staib, Eshref Yozdemir and collaborators \size default \begin_inset Newline newline @@ -19369,6 +19369,155 @@ pspm_compute_visual_angle to mm. \end_layout +\begin_layout Standard +\begin_inset Separator plain +\end_inset + + +\end_layout + +\begin_layout Section +PsPM Version 4.4.0 +\end_layout + +\begin_layout Subsection* +New Features +\end_layout + +\begin_layout Itemize + +\family typewriter +pspm_data_editor +\family default + supports loading in an epoch file, and +\family typewriter +pspm_pp (simple_qa) +\family default + now supports an output file for the epochs of data that is filtered out. +\end_layout + +\begin_layout Itemize + +\family typewriter +pspm_convert_gaze_distance +\family default +introduced to allow converting from distance units to degrees and scanpath + speed. + The accompanying GUI editor is availble under 'Gaze Convert' in the data + processing section. +\end_layout + +\begin_layout Itemize +Add the posibility to select the flank in the +\family typewriter +Import +\family default + module of the GUI of PsPM. +\end_layout + +\begin_layout Subsection* +Changes +\end_layout + +\begin_layout Itemize +The data convert functionality in tools is now split into the Gaze Convert + and Pupil Size convert in Data preprocessing. +\end_layout + +\begin_layout Itemize +Factor out blink/saccade edge filtering logic out of +\family typewriter +pspm_get_eyelink +\family default + to +\family typewriter +pspm_blink_saccade_filt +\family default +. +\end_layout + +\begin_layout Itemize +Deprecate edge filtering functionality in +\family typewriter +pspm_get_eyelink +\family default +. +\end_layout + +\begin_layout Itemize +Make +\family typewriter +pspm_pupil_correct_eyelink +\family default + use the last gaze channel when there are multiple gaze x (similarly y) + channels in the file. +\end_layout + +\begin_layout Itemize +Make +\family typewriter +pspm_extract_segments +\family default + return NaN percentages and not ratios. +\end_layout + +\begin_layout Subsection* +Bugfixes +\end_layout + +\begin_layout Itemize +DCM plot XTick is now scaled by sample rate. +\end_layout + +\begin_layout Itemize +Correct index offset when dealing with the descending flank for continuous + markers. +\end_layout + +\begin_layout Itemize +Allow +\family typewriter +pspm_display +\family default + to plot any type of marker channels. +\end_layout + +\begin_layout Itemize +Fix +\family typewriter +pspm_display +\family default + behaviour when user tries to load data with less channels than the data + he/she is curretly displaying. +\end_layout + +\begin_layout Itemize +Fix +\family typewriter +pspm_split_sessions +\family default + behaviour when the intertrial interval in the data is random. + Add an option +\family typewriter +randomITI +\family default + (0 or 1) which in the latter case it forces the function to evaluate the + mean marker distance using all the markers and not per session. + Default value: 0. +\end_layout + +\begin_layout Itemize +Remove +\family typewriter +startsWith +\family default + and +\family typewriter +endsWith +\family default + from all functions for a better backward compatibility. +\end_layout + \begin_layout Part Acknowledgements \end_layout diff --git a/doc/PsPM_release_checklist.md b/doc/PsPM_release_checklist.md index 6455615a8..eb57040a9 100644 --- a/doc/PsPM_release_checklist.md +++ b/doc/PsPM_release_checklist.md @@ -11,7 +11,7 @@ release branch after making absolutely sure that no new stuff will be implemente - [x] `pspm.fig`: Load `pspm.fig` into MATLAB, update `fig.Children(9).String` and save back to `pspm.fig` - [x] Manual and Developers Guide: front pages - [x] Make sure both manuals are updated -- [x] Add release notes section of the new version to manual (at the end) +- [x] Add release notes section of the new version to manual (at the end) and release_notes.tex - [x] Get the manual reviewed - [x] Create manual and dev guide PDFs using `lyx` - [x] Check if underscores and dashes are visible in newly added manual sections diff --git a/doc/release_notes.tex b/doc/release_notes.tex index b99d9ba8b..0a588521a 100644 --- a/doc/release_notes.tex +++ b/doc/release_notes.tex @@ -607,6 +607,52 @@ \subsection*{Bugfixes} an error in the conversion factor of pixels wrt. to mm. \end{itemize} +\section{PsPM Version 4.4.0} + +\subsection*{New Features} +\begin{itemize} +\item \texttt{pspm\_data\_editor} supports loading in an epoch file, and +\texttt{pspm\_pp (simple\_qa)} now supports an output file for the +epochs of data that is filtered out. +\item \texttt{pspm\_convert\_gaze\_distance }introduced to allow converting +from distance units to degrees and scanpath speed. The accompanying +GUI editor is availble under 'Gaze Convert' in the data processing +section. +\item Add the posibility to select the flank in the \texttt{Import} module +of the GUI of PsPM. +\end{itemize} + +\subsection*{Changes} +\begin{itemize} +\item The data convert functionality in tools is now split into the Gaze +Convert and Pupil Size convert in Data preprocessing. +\item Factor out blink/saccade edge filtering logic out of \texttt{pspm\_get\_eyelink} +to \texttt{pspm\_blink\_saccade\_filt}. +\item Deprecate edge filtering functionality in \texttt{pspm\_get\_eyelink}. +\item Make \texttt{pspm\_pupil\_correct\_eyelink} use the last gaze channel +when there are multiple gaze x (similarly y) channels in the file. +\item Make \texttt{pspm\_extract\_segments} return NaN percentages and not +ratios. +\end{itemize} + +\subsection*{Bugfixes} +\begin{itemize} +\item DCM plot XTick is now scaled by sample rate. +\item Correct index offset when dealing with the descending flank for continuous +markers. +\item Allow \texttt{pspm\_display} to plot any type of marker channels. +\item Fix \texttt{pspm\_display} behaviour when user tries to load data +with less channels than the data he/she is curretly displaying. +\item Fix \texttt{pspm\_split\_sessions} behaviour when the intertrial interval +in the data is random. Add an option \texttt{randomITI} (0 or 1) which +in the latter case it forces the function to evaluate the mean marker +distance using all the markers and not per session. Default value: +0. +\item Remove \texttt{startsWith} and \texttt{endsWith} from all functions +for a better backward compatibility. +\end{itemize} + + \section{References} \bibliographystyle{pnas2009} diff --git a/src/pspm.fig b/src/pspm.fig index 6056db80998a0afc3ca4a33310de293b4086c49a..19cd46d905c3d2308185a9df6e3e7fdef7a87732 100644 GIT binary patch delta 8844 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 diff --git a/src/pspm_msg.txt b/src/pspm_msg.txt index 888f1989f..3422fc386 100644 --- a/src/pspm_msg.txt +++ b/src/pspm_msg.txt @@ -1,10 +1,10 @@ $___________________________________________________________________________ Welcome to PsPM - PsychoPhysiological Modelling (incorporating SCRalyze) -Version 4.3.0 (25.03.2020) +Version 4.4.0 (21.08.2020) $ ------------------------------------------ -(c) 2008-2019 +(c) 2008-2020 Dominik R Bach * Wellcome Trust Centre for Neuroimaging University College London diff --git a/src/pspm_quit.m b/src/pspm_quit.m index a2973f0c0..90ffb98d8 100644 --- a/src/pspm_quit.m +++ b/src/pspm_quit.m @@ -1,6 +1,6 @@ % pspm_quit clears settings, removes paths & closes figures %__________________________________________________________________________ -% PsPM 4.3.0 +% PsPM 4.4.0 % (C) 2008-2019 Dominik R Bach (Wellcome Trust Centre for Neuroimaging) % % $Id: pspm_quit.m 805 2019-09-16 07:12:08Z esrefo $ diff --git a/test/pspm_pp_test.m b/test/pspm_pp_test.m index 9e8b38a61..55267d1da 100644 --- a/test/pspm_pp_test.m +++ b/test/pspm_pp_test.m @@ -3,7 +3,7 @@ % unittest class for the pspm_pp function %__________________________________________________________________________ % SCRalyze TestEnvironment -% (C) 2013 Linus Rüttimann (University of Zurich) +% (C) 2013 Linus R�ttimann (University of Zurich) properties end From 67350998552949e0b11dffd81a795eccb04e8d79 Mon Sep 17 00:00:00 2001 From: Sam Maxwell Date: Thu, 27 Aug 2020 11:26:40 +0100 Subject: [PATCH 17/31] Merge pull request #152 from bachlab/refactor/remove-deprecated-edge-filtering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(remove-edge-filtering): remove edge filtering from get eyeli… --- src/pspm_cfg/pspm_cfg_import.m | 30 +----------------- src/pspm_cfg/pspm_cfg_run_import.m | 4 --- src/pspm_get_eyelink.m | 50 +----------------------------- 3 files changed, 2 insertions(+), 82 deletions(-) diff --git a/src/pspm_cfg/pspm_cfg_import.m b/src/pspm_cfg/pspm_cfg_import.m index 0b95895e8..354f2b75f 100644 --- a/src/pspm_cfg/pspm_cfg_import.m +++ b/src/pspm_cfg/pspm_cfg_import.m @@ -115,34 +115,6 @@ 'enabled (> 0) the data will be converted from arbitrary units to ', ... 'length units.']}; -eyelink_edge_discard_factor = cfg_entry; -eyelink_edge_discard_factor.name = 'Blink/saccade discard factor'; -eyelink_edge_discard_factor.tag = 'eyelink_edge_discard_factor'; -eyelink_edge_discard_factor.val = {0}; -eyelink_edge_discard_factor.num = [1 1]; -eyelink_edge_discard_factor.strtype = 'r'; -eyelink_edge_discard_factor.help = {['Factor used to determine the number of', ... - ' samples right before and right after a blink/saccade', ... - ' period to discard. This value is multiplied by the', ... - ' sampling rate of the recording to determine the', ... - ' number of samples to discard from one end. Therefore,', ... - ' for each blink/saccade period, 2*this_value*SR many', ... - ' samples are discarded in total, and effectively', ... - ' blink/saccade period is extended.'], ... - - ['This value also corresponds to the duration of', ... - ' samples to discard on one end in seconds. For example,', ... - ' when it is 0.01, we discard 10 ms worth of data on', ... - ' each end of every blink/saccade period.'] ... - - ['The default value has been changed to 0 in PsPM revision', ... - ' r803 to reduce the amount of discarded data. Note that', ... - ' this might result in noisy samples around blink/saccade', ... - ' points. Therefore, it is highly recommended to perform', ... - ' pupil size data preprocessing and gaze data filtering by', ... - ' finding valid fixations.'] ... -}; - distance_unit = cfg_menu; distance_unit.name = 'Distance unit'; distance_unit.tag = 'distance_unit'; @@ -364,7 +336,7 @@ % Refactor this part by even possibly dividing pspm_cfg_import to several files. if any(strcmp(settings.import.datatypes(datatype_i).short, 'eyelink')) datatype_item{datatype_i}.val = ... - [datatype_item{datatype_i}.val, {eyelink_trackdist, eyelink_edge_discard_factor, distance_unit}]; + [datatype_item{datatype_i}.val, {eyelink_trackdist, distance_unit}]; end if any(strcmpi(settings.import.datatypes(datatype_i).short, 'viewpoint')) diff --git a/src/pspm_cfg/pspm_cfg_run_import.m b/src/pspm_cfg/pspm_cfg_run_import.m index 14ab4e60b..5ba695a35 100644 --- a/src/pspm_cfg/pspm_cfg_run_import.m +++ b/src/pspm_cfg/pspm_cfg_run_import.m @@ -84,10 +84,6 @@ end end - if isfield(job.datatype.(datatype), 'eyelink_edge_discard_factor') - import{i}.blink_saccade_edge_discard_factor = job.datatype.(datatype).eyelink_edge_discard_factor; - end - if isfield(job.datatype.(datatype), 'viewpoint_target_unit') import{i}.target_unit = job.datatype.(datatype).viewpoint_target_unit; end diff --git a/src/pspm_get_eyelink.m b/src/pspm_get_eyelink.m index 49c89f34e..3f124ca9b 100644 --- a/src/pspm_get_eyelink.m +++ b/src/pspm_get_eyelink.m @@ -24,34 +24,6 @@ % the unit to which the data should be converted and % in which eyelink_trackdist is given % - % .blink_saccade_edge_discard_factor: - % DEPRECATED: This option is deprecated, and will be - % removed in the next API breaking PsPM release. Please - % change your codes to use the new - % pspm_blink_saccade_filt function if you rely on this - % functionality. - % - % Factor used to determine the number of - % samples right before and right after a blink/saccade - % period to discard. This value is multiplied by the - % sampling rate of the recording to determine the - % number of samples to discard from one end. Therefore, - % for each blink/saccade period, 2*this_value*SR many - % samples are discarded in total, and effectively - % blink/saccade period is extended. - % - % This value also corresponds to the duration of - % samples to discard on one end in seconds. For example, - % when it is 0.01, we discard 10 ms worth of data on - % each end of every blink/saccade period. - % - % The default value has been changed to 0 in PsPM revision - % r803 to reduce the amount of discarded data. Note that - % this might result in noisy samples around blink/saccade - % points. Therefore, it is highly recommended to perform - % pupil size data preprocessing using pspm_pupil_pp and - % gaze data filtering using pspm_find_valid_fixations. - % (Default: 0) % % % In this function, channels related to eyes will not produce an error, if @@ -72,22 +44,6 @@ sourceinfo = []; sts = -1; % add specific import path for specific import function addpath(pspm_path('Import','eyelink')); - default_blink_saccade_discard_factor = 0; - for i = 1:numel(import) - if ~isfield(import{i}, 'blink_saccade_edge_discard_factor') - import{i}.blink_saccade_edge_discard_factor = default_blink_saccade_discard_factor; - else - warning('ID:deprecated', ['pspm_get_eyelink: blink_saccade_edge_discard_factor argument is DEPRECATED.'... - 'Please change your code to use the new pspm_blink_saccade_filt function if you rely on this'... - 'functionality.']); - end - - if ~isnumeric(import{i}.blink_saccade_edge_discard_factor) || ... - import{i}.blink_saccade_edge_discard_factor < 0 - warning('ID:invalid_input', 'Edge discard factor must be a positive number'); - return; - end - end % transfer options % ------------------------------------------------------------------------- @@ -113,11 +69,7 @@ mask_chans = {'blink_l', 'blink_r', 'saccade_l', 'saccade_r'}; end expand_factor = 0; - if i <= numel(import) - expand_factor = import{i}.blink_saccade_edge_discard_factor; - else - expand_factor = default_blink_saccade_discard_factor; - end + data{i}.channels = blink_saccade_filtering(... data{i}.channels, ... data{i}.channels_header, ... From 6c7d7b4288598d948ee8f99a09109fe668d55228 Mon Sep 17 00:00:00 2001 From: irojkov-ph <56234933+irojkov-ph@users.noreply.github.com> Date: Fri, 28 Aug 2020 10:16:21 +0200 Subject: [PATCH 18/31] Merge pull request #151 from bachlab/feat/dsv-110 Feat/dsv 110 --- doc/PsPM_Manual.lyx | 13 ++++++ doc/release_notes.tex | 7 +++ src/pspm_cfg/pspm_cfg_import.m | 10 ++++ src/pspm_cfg/pspm_cfg_run_import.m | 4 ++ src/pspm_get_csv.m | 19 ++++++++ src/pspm_get_txt.m | 41 ++++++++++++----- src/pspm_import.m | 4 +- src/pspm_init.m | 74 ++++++++++++++++++++++-------- test/pspm_get_txt_test.m | 42 ++++++++++++++++- 9 files changed, 180 insertions(+), 34 deletions(-) create mode 100644 src/pspm_get_csv.m diff --git a/doc/PsPM_Manual.lyx b/doc/PsPM_Manual.lyx index b08ce7d3d..0095ab07b 100644 --- a/doc/PsPM_Manual.lyx +++ b/doc/PsPM_Manual.lyx @@ -19518,6 +19518,19 @@ endsWith from all functions for a better backward compatibility. \end_layout +\begin_layout Section +PsPM Version 5.0.0 +\end_layout + +\begin_layout Subsection* +New Features +\end_layout + +\begin_layout Itemize +New DSV (delimiter separated values) option for imports as well as a CSV + import function. +\end_layout + \begin_layout Part Acknowledgements \end_layout diff --git a/doc/release_notes.tex b/doc/release_notes.tex index 0a588521a..4b1c91177 100644 --- a/doc/release_notes.tex +++ b/doc/release_notes.tex @@ -653,6 +653,13 @@ \subsection*{Bugfixes} \end{itemize} +\section{PsPM Version 5.0.0} + +\subsection*{New Features} +\begin{itemize} +\item New DSV (delimiter separated values) option for imports as well as a CSV import function +\end{itemize} + \section{References} \bibliographystyle{pnas2009} diff --git a/src/pspm_cfg/pspm_cfg_import.m b/src/pspm_cfg/pspm_cfg_import.m index 354f2b75f..0a6f923ba 100644 --- a/src/pspm_cfg/pspm_cfg_import.m +++ b/src/pspm_cfg/pspm_cfg_import.m @@ -148,6 +148,12 @@ smi_stimulus_resolution.help = {['The resolution of the stimulus window. This field is required' ... 'to perform px to mm conversions for gaze channels']}; +delimiter = cfg_entry; +delimiter.name = 'Delimiter'; +delimiter.tag = 'delimiter'; +delimiter.strtype = 's'; +delimiter.help = {['The delimiter to be used for file reading, leave blank to use any whitespace character.']}; + %% Datatype dependend items datatype_item = cell(1,length(fileoptions)); for datatype_i=1:length(fileoptions) @@ -346,6 +352,10 @@ if any(strcmpi(settings.import.datatypes(datatype_i).short, 'smi')) datatype_item{datatype_i}.val = [datatype_item{datatype_i}.val, {smi_target_unit, smi_stimulus_resolution}]; end + + if any(strcmpi(settings.import.datatypes(datatype_i).short, 'dsv')) + datatype_item{datatype_i}.val = [datatype_item{datatype_i}.val, {delimiter}]; + end end %% Data type diff --git a/src/pspm_cfg/pspm_cfg_run_import.m b/src/pspm_cfg/pspm_cfg_run_import.m index 5ba695a35..f308603a2 100644 --- a/src/pspm_cfg/pspm_cfg_run_import.m +++ b/src/pspm_cfg/pspm_cfg_run_import.m @@ -92,6 +92,10 @@ import{i}.target_unit = job.datatype.(datatype).smi_target_unit; import{i}.stimulus_resolution = job.datatype.(datatype).smi_stimulus_resolution; end + + if isfield(job.datatype.(datatype), 'delimiter') + import{i}.delimiter = job.datatype.(datatype).delimiter; + end end end diff --git a/src/pspm_get_csv.m b/src/pspm_get_csv.m new file mode 100644 index 000000000..5471c4958 --- /dev/null +++ b/src/pspm_get_csv.m @@ -0,0 +1,19 @@ +function [sts, import, sourceinfo] = pspm_get_csv(datafile, import) +% pspm_get_csv is the main function for import of csv files, +% it adds the comma delimiter to import channels and the runs pspm_get_txt +% +% FORMAT: [sts, import, sourceinfo] = pspm_get_csv(datafile, import); +% datafile: a .csv or .txt file containing numerical data with comma delimiter +% and optionally the channel names in the first line. +% import: import job structure +% A delimiter of ',' is applied to all import channels +% +%__________________________________________________________________________ +% PsPM 5.0 +% (C) 2008-2020 Dominik R Bach (Wellcome Trust Centre for Neuroimaging) + +% $Id$ +% $Rev$ + +import = cellfun(@(c) setfield(c, 'delimiter', ','), import, 'UniformOutput', false); +[sts, import, sourceinfo] = pspm_get_text(datafile, import); \ No newline at end of file diff --git a/src/pspm_get_txt.m b/src/pspm_get_txt.m index 8cd52b826..0e4a5285b 100644 --- a/src/pspm_get_txt.m +++ b/src/pspm_get_txt.m @@ -2,9 +2,13 @@ % pspm_get_txt is the main function for import of text files % % FORMAT: [sts, import, sourceinfo] = pspm_get_txt(datafile, import); -% datafile: a .txt-file containing numerical data (with any -% delimiter) and optionally the channel names in the first -% line. +% datafile: a .txt-file containing numerical data (with any +% delimiter) and optionally the channel names in the first +% line. +% import: import job structure +% If a delimiter is to be used, provide a delimiter field and value on the first import cell +% for example, such that import{1}.delimiter == ',' +% %__________________________________________________________________________ % PsPM 3.0 % (C) 2008-2015 Dominik R Bach (Wellcome Trust Centre for Neuroimaging) @@ -28,20 +32,34 @@ % load & check data % ------------------------------------------------------------------------- fid = fopen(datafile); -channel_names = textscan(fgetl(fid), '%s'); + +if isfield(import{1}, 'delimiter') + channel_names = textscan(fgetl(fid), '%s', 'Delimiter', import{1}.delimiter); +else + channel_names = textscan(fgetl(fid), '%s'); +end + + channel_names = channel_names{1}; fclose(fid); - fline = str2double(channel_names); if ~any(isnan(fline)) %no headerline - data = dlmread(datafile); + if nargin == 3; + data = dlmread(datafile, delimiter); + else; + data = dlmread(datafile); + end; + elseif all(isnan(fline)) %headerline fid = fopen(datafile); - formatSpec = ''; - for i=1:numel(channel_names) - formatSpec = [formatSpec '%f']; - end - data = textscan(fid, formatSpec, 'HeaderLines', 1); + formatSpec = repmat('%f', 1, numel(channel_names)); + + % if delimiter provided + if isfield(import{1}, 'delimiter') + data = textscan(fid, formatSpec, 'HeaderLines', 1, 'Delimiter', import{1}.delimiter); + else; + data = textscan(fid, formatSpec, 'HeaderLines', 1); + end; data = cell2mat(data); fclose(fid); else @@ -49,7 +67,6 @@ end if isempty(data), warning('An error occured while reading a textfile.\n'); return; end; - % select desired channels % ------------------------------------------------------------------------- diff --git a/src/pspm_import.m b/src/pspm_import.m index 1848ef12a..3468b02d3 100644 --- a/src/pspm_import.m +++ b/src/pspm_import.m @@ -32,7 +32,9 @@ % Can be one of the following units:'mm', 'cm', 'm','inches'. % - .denoise: for marker channels in CED spike format (recorded % as 'level'), filters out markers duration longer than the -% value given here (in ms) +% value given here (in ms) +% - .delimiter: for delimiter separated values, value used as delimiter for file read +% % options: options.overwrite - overwrite existing files by default % % RETURNS diff --git a/src/pspm_init.m b/src/pspm_init.m index f0a6ab6f8..b65bd9bfd 100644 --- a/src/pspm_init.m +++ b/src/pspm_init.m @@ -484,9 +484,43 @@ 'comma as used in some non-English speaking countries). At the moment, ', ... 'no import of event markers is possible']); +% Delimiter Separated files +% ---------- +defaults.import.datatypes(4) = ... + struct('short', 'dsv', ... + 'long', 'Delimiter Separated Values', ... + 'ext', 'any', ... + 'funct', @pspm_get_txt, ... + 'chantypes', {{defaults.importchantypes(strcmpi('wave',{defaults.importchantypes.data}) | ... + strcmpi('marker', {defaults.importchantypes.type})).type}}, ... %all wave channels + marker + 'chandescription', 'column', ... + 'multioption', 1, ... + 'searchoption', 1, ... + 'automarker', 0, ... + 'autosr', 0, ... + 'help', ['Reads a file using a custom delimiter, for example',... + 'a delimiter or a comma (,) would read the same as a csv']); + +% CSV - copy of dsv with partially applied delimiter +defaults.import.datatypes(5) = ... + struct('short', 'csv', ... + 'long', 'Comma Separated Values', ... + 'ext', 'csv', ... + 'funct', @pspm_get_csv, ... + 'chantypes', {{defaults.importchantypes(strcmpi('wave',{defaults.importchantypes.data}) | ... + strcmpi('marker', {defaults.importchantypes.type})).type}}, ... %all wave channels + marker + 'chandescription', 'column', ... + 'multioption', 1, ... + 'searchoption', 1, ... + 'automarker', 0, ... + 'autosr', 0, ... + 'help', ['Read using comma as a delimiter']); + + + % Biopac Acknowledge up to version 3.9.0 % -------------------------------------- -defaults.import.datatypes(4) = ... +defaults.import.datatypes(6) = ... struct('short', 'acq', ... 'long', 'Biopac Acqknowledge 3.9.0 or lower (.acq)', ... 'ext', 'acq', ... @@ -501,7 +535,7 @@ % exported Biopac Acqknowledge (tested on version 4.2.0) % ----------------------------------------------------- -defaults.import.datatypes(5) = ... +defaults.import.datatypes(7) = ... struct('short', 'acqmat', ... 'long', 'matlab-exported Biopac Acqknowledge 4.0 or higher', ... 'ext', 'mat', ... @@ -516,7 +550,7 @@ % bioread converted Biopac Acqknowledge (any version) % ----------------------------------------------------- -defaults.import.datatypes(6) = ... +defaults.import.datatypes(8) = ... struct('short', 'acq_bioread', ... 'long', 'bioread-converted Biopac Acqknowledge (any version)', ... 'ext', 'mat', ... @@ -535,7 +569,7 @@ % ADInstruments Labchart (any Version) % ----------------------------------------------------- -defaults.import.datatypes(7) = ... +defaults.import.datatypes(9) = ... struct('short', 'labchartmat', ... 'long', 'ADInstruments LabChart (any Version, Windows only)', ... 'ext', 'adicht', ... @@ -552,7 +586,7 @@ % exported ADInstruments Labchart up to 7.1 % ----------------------------------------------------- -defaults.import.datatypes(8) = ... +defaults.import.datatypes(10) = ... struct('short', 'labchartmat_ext', ... 'long', 'matlab-exported ADInstruments LabChart 7.1 or lower', ... 'ext', 'mat', ... @@ -568,7 +602,7 @@ % exported ADInstruments Labchart 7.2 or higher % ----------------------------------------------------- -defaults.import.datatypes(9) = ... +defaults.import.datatypes(11) = ... struct('short', 'labchartmat_in', ... 'long', 'matlab-exported ADInstruments LabChart 7.2 or higher', ... 'ext', 'mat', ... @@ -583,7 +617,7 @@ % VarioPort % ----------------------------------------------------- -defaults.import.datatypes(10) = ... +defaults.import.datatypes(12) = ... struct('short', 'vario', ... 'long', 'VarioPort (.vdp)', ... 'ext', 'vpd', ... @@ -598,7 +632,7 @@ % exported Biograph Infiniti % ----------------------------------------------------- -defaults.import.datatypes(11) = ... +defaults.import.datatypes(13) = ... struct('short', 'biograph', ... 'long', 'text-exported Biograph Infiniti', ... 'ext', 'txt', ... @@ -614,7 +648,7 @@ % exported MindMedia Biotrace % ----------------------------------------------------- -defaults.import.datatypes(12) = ... +defaults.import.datatypes(14) = ... struct('short', 'biotrace', ... 'long', 'text-exported MindMedia Biotrace', ... 'ext', 'txt', ... @@ -629,7 +663,7 @@ % Brain Vision % ----------------------------------------------------- -defaults.import.datatypes(13) = ... +defaults.import.datatypes(15) = ... struct('short', 'brainvision', ... 'long', 'BrainVision (.eeg)', ... 'ext', 'eeg', ... @@ -644,7 +678,7 @@ % Dataq Windaq (e. g. provided by Coulbourn Instruments) % ----------------------------------------------------- -defaults.import.datatypes(14) = ... +defaults.import.datatypes(16) = ... struct('short', 'windaq', ... 'long', 'DATAQ Windaq (.wdq) (read with ActiveX-Lib)', ... 'ext', 'wdq', ... @@ -661,7 +695,7 @@ % Dataq Windaq (PsPM Version) % ----------------------------------------------------- -defaults.import.datatypes(15) = ... +defaults.import.datatypes(17) = ... struct('short', 'windaq_n', ... 'long', 'DATAQ Windaq (.wdq)', ... 'ext', 'wdq', ... @@ -683,7 +717,7 @@ % Noldus Observer XT compatible .txt files % ----------------------------------------------------- -defaults.import.datatypes(16) = ... +defaults.import.datatypes(18) = ... struct('short', 'observer', ... 'long', 'Noldus Observer XT compatible text file', ... 'ext', 'any', ... @@ -698,7 +732,7 @@ % NeuroScan % ----------------------------------------------------- -defaults.import.datatypes(17) = ... +defaults.import.datatypes(19) = ... struct('short', 'cnt', ... 'long', 'Neuroscan (.cnt)', ... 'ext', 'cnt', ... @@ -713,7 +747,7 @@ % BioSemi % ----------------------------------------------------- -defaults.import.datatypes(18) = ... +defaults.import.datatypes(20) = ... struct('short', 'biosemi', ... 'long', 'BioSemi (.bdf)', ... 'ext', 'bdf', ... @@ -728,7 +762,7 @@ % Eyelink 1000 files % --------------------------------------------- -defaults.import.datatypes(19) = ... +defaults.import.datatypes(21) = ... struct('short', 'eyelink', ... 'long', 'Eyelink 1000 (.asc)', ... 'ext', 'asc', ... @@ -760,7 +794,7 @@ % European Data Format (EDF) % ----------------------------------------------------- -defaults.import.datatypes(20) = ... +defaults.import.datatypes(22) = ... struct('short', 'edf', ... 'long', 'European Data Format (.edf)', ... 'ext', 'edf', ... @@ -775,7 +809,7 @@ % Philips Scanphyslog (.log) % ----------------------------------------------------- -defaults.import.datatypes(21) = ... +defaults.import.datatypes(23) = ... struct('short', 'physlog', ... 'long', 'Philips Scanphyslog (.log)', ... 'ext', 'log', ... @@ -803,7 +837,7 @@ % ViewPoint EyeTracker files % --------------------------------------------- -defaults.import.datatypes(22) = ... +defaults.import.datatypes(24) = ... struct('short', 'viewpoint', ... 'long', 'ViewPoint EyeTracker (.txt)', ... 'ext', 'txt', ... @@ -820,7 +854,7 @@ % SMI EyeTracker files % --------------------------------------------- -defaults.import.datatypes(23) = ... +defaults.import.datatypes(25) = ... struct('short', 'smi', ... 'long', 'SensoMotoric Instruments iView X EyeTracker (.txt)', ... 'ext', 'txt', ... diff --git a/test/pspm_get_txt_test.m b/test/pspm_get_txt_test.m index 254f102ad..7dd3b44ad 100644 --- a/test/pspm_get_txt_test.m +++ b/test/pspm_get_txt_test.m @@ -3,7 +3,7 @@ % unittest class for the pspm_get_txt function %__________________________________________________________________________ % SCRalyze TestEnvironment -% (C) 2013 Linus Rüttimann (University of Zurich) +% (C) 2013 Linus R�ttimann (University of Zurich) properties testcases; @@ -45,6 +45,46 @@ function define_testcases(this) fprintf(fid,'%f\t%f\t%f\t%f\n', data(k,1), data(k,2), data(k,3), data(k,4)); end fclose(fid); + + %testcase 3 (csv with header) + %-------------------------------------------------------------- + this.testcases{3}.pth = 'testdatafile132435.csv'; + + this.testcases{3}.import{1} = struct('type', 'scr' , 'channel', 0, 'sr', 100, 'delimiter', ','); + this.testcases{3}.import{2} = struct('type', 'ecg' , 'channel', 0, 'sr', 100, 'delimiter', ','); + this.testcases{3}.import{3} = struct('type', 'hr' , 'channel', 0, 'sr', 100, 'delimiter', ','); + this.testcases{3}.import{4} = struct('type', 'resp' , 'channel', 0, 'sr', 100, 'delimiter', ','); + + %generate testdata + header = {'scr' 'ecg' 'heart' 'resp'}; + data = rand(900, 4); + + fid = fopen(this.testcases{3}.pth, 'w'); + fprintf(fid,'scr,ecg,rate,resp\n'); + for k=1:size(data,1) + fprintf(fid,'%f,%f,%f,%f\n', data(k,1), data(k,2), data(k,3), data(k,4)); + end + fclose(fid); + + %testcase 4 (delimiter separated value with custom delimiter (|)) + %-------------------------------------------------------------- + this.testcases{4}.pth = 'testdatafile132435.psv'; + + this.testcases{4}.import{1} = struct('type', 'scr' , 'channel', 0, 'sr', 100, 'delimiter', '|'); + this.testcases{4}.import{2} = struct('type', 'ecg' , 'channel', 0, 'sr', 100, 'delimiter', '|'); + this.testcases{4}.import{3} = struct('type', 'hr' , 'channel', 0, 'sr', 100, 'delimiter', '|'); + this.testcases{4}.import{4} = struct('type', 'resp' , 'channel', 0, 'sr', 100, 'delimiter', '|'); + + %generate testdata + header = {'scr' 'ecg' 'heart' 'resp'}; + data = rand(900, 4); + + fid = fopen(this.testcases{4}.pth, 'w'); + fprintf(fid,'scr|ecg|rate|resp\n'); + for k=1:size(data,1) + fprintf(fid,'%f|%f|%f|%f\n', data(k,1), data(k,2), data(k,3), data(k,4)); + end + fclose(fid); end end From 80229cb5e045ee07b375a4a0fc1e9211350f9b27 Mon Sep 17 00:00:00 2001 From: irojkov-ph <56234933+irojkov-ph@users.noreply.github.com> Date: Mon, 7 Sep 2020 09:09:22 +0200 Subject: [PATCH 19/31] Merge pull request #156 from bachlab/feat/dsv-110 Feat/dsv 110 --- src/pspm_cfg/pspm_cfg_import.m | 37 ++++++- src/pspm_cfg/pspm_cfg_run_import.m | 13 +++ src/pspm_get_csv.m | 2 +- src/pspm_get_txt.m | 160 +++++++++++++++++++++-------- test/pspm_get_txt_test.m | 43 ++++++-- 5 files changed, 202 insertions(+), 53 deletions(-) diff --git a/src/pspm_cfg/pspm_cfg_import.m b/src/pspm_cfg/pspm_cfg_import.m index 0a6f923ba..b569a75fd 100644 --- a/src/pspm_cfg/pspm_cfg_import.m +++ b/src/pspm_cfg/pspm_cfg_import.m @@ -152,7 +152,30 @@ delimiter.name = 'Delimiter'; delimiter.tag = 'delimiter'; delimiter.strtype = 's'; -delimiter.help = {['The delimiter to be used for file reading, leave blank to use any whitespace character.']}; +delimiter.help = {'The delimiter to be used for file reading, leave blank to use any whitespace character.'}; + +header_lines = cfg_entry; +header_lines.name = 'Header lines'; +header_lines.tag = 'header_lines'; +header_lines.strtype = 'r'; +header_lines.val = {1}; +header_lines.help = {'The number of lines used by the header. By default 1.'}; + +channel_names_line = cfg_entry; +channel_names_line.name = 'Channel names line'; +channel_names_line.tag = 'channel_names_line'; +channel_names_line.strtype = 'r'; +channel_names_line.val = {1}; +channel_names_line.help = {'The line number where the channel/column names are specified. By default 1.'}; + +exclude_columns = cfg_entry; +exclude_columns.name = 'Exclude columns'; +exclude_columns.tag = 'exclude_columns'; +exclude_columns.strtype = 'r'; +exclude_columns.val = {0}; +exclude_columns.help = {['The number of columns which have to be excluded for the importing. By default 0. ',... + 'It is usefull if the first columns have non numeric data (e.g. timestamps). ', ... + 'Be aware that if you exclude some columns you have to adapt the channel number.']}; %% Datatype dependend items datatype_item = cell(1,length(fileoptions)); @@ -352,9 +375,17 @@ if any(strcmpi(settings.import.datatypes(datatype_i).short, 'smi')) datatype_item{datatype_i}.val = [datatype_item{datatype_i}.val, {smi_target_unit, smi_stimulus_resolution}]; end - + + if any(strcmpi(settings.import.datatypes(datatype_i).short, 'txt')) + datatype_item{datatype_i}.val = [datatype_item{datatype_i}.val, {header_lines,channel_names_line,exclude_columns}]; + end + + if any(strcmpi(settings.import.datatypes(datatype_i).short, 'csv')) + datatype_item{datatype_i}.val = [datatype_item{datatype_i}.val, {header_lines,channel_names_line,exclude_columns}]; + end + if any(strcmpi(settings.import.datatypes(datatype_i).short, 'dsv')) - datatype_item{datatype_i}.val = [datatype_item{datatype_i}.val, {delimiter}]; + datatype_item{datatype_i}.val = [datatype_item{datatype_i}.val, {delimiter,header_lines,channel_names_line,exclude_columns}]; end end diff --git a/src/pspm_cfg/pspm_cfg_run_import.m b/src/pspm_cfg/pspm_cfg_run_import.m index f308603a2..256cc0bd3 100644 --- a/src/pspm_cfg/pspm_cfg_run_import.m +++ b/src/pspm_cfg/pspm_cfg_run_import.m @@ -96,6 +96,19 @@ if isfield(job.datatype.(datatype), 'delimiter') import{i}.delimiter = job.datatype.(datatype).delimiter; end + + if isfield(job.datatype.(datatype), 'header_lines') + import{i}.header_lines = job.datatype.(datatype).header_lines; + end + + if isfield(job.datatype.(datatype), 'channel_names_line') + import{i}.channel_names_line = job.datatype.(datatype).channel_names_line; + end + + if isfield(job.datatype.(datatype), 'exclude_columns') + import{i}.exclude_columns = job.datatype.(datatype).exclude_columns; + end + end end diff --git a/src/pspm_get_csv.m b/src/pspm_get_csv.m index 5471c4958..e9d3a7c31 100644 --- a/src/pspm_get_csv.m +++ b/src/pspm_get_csv.m @@ -16,4 +16,4 @@ % $Rev$ import = cellfun(@(c) setfield(c, 'delimiter', ','), import, 'UniformOutput', false); -[sts, import, sourceinfo] = pspm_get_text(datafile, import); \ No newline at end of file +[sts, import, sourceinfo] = pspm_get_txt(datafile, import); \ No newline at end of file diff --git a/src/pspm_get_txt.m b/src/pspm_get_txt.m index 0e4a5285b..c3297c6ec 100644 --- a/src/pspm_get_txt.m +++ b/src/pspm_get_txt.m @@ -6,22 +6,47 @@ % delimiter) and optionally the channel names in the first % line. % import: import job structure -% If a delimiter is to be used, provide a delimiter field and value on the first import cell -% for example, such that import{1}.delimiter == ',' -% +% - required fields: +% .type: +% A char array corresponding to a valid PsPM data +% type, see `pspm_init.m` for more details. +% .channel: +% A numeric value representing the column number +% of the corresponding numerical data. +% - optional fields: +% .delimiter: +% A char array corresponding to the delimiter +% used in the datafile to delimit data columns. +% To be used it should be specified on the first +% import cell, e.g.: +% import{1}.delimiter == ',' +% Default: white-space (see textscan function) +% .header_lines: +% A numeric value corresponding to the number of +% header lines. Which means the data start on +% line number: "header_lines + 1". +% To be used it should be specified on the first +% import cell, e.g.: +% import{1}.header_lines == 3 +% Default: 1. +% .channel_names_line: +% A numeric value corresponding to the line +% number where the channel names are specified. +% To be used it should be specified on the first +% import cell, e.g.: +% import{1}.channel_names_line == 2 +% Default: 1. +% .exclude_columns: +% A numeric value corresponding to the number of +% columns to exclude starting from the left. +% To be used it should be specified on the first +% import cell, e.g.: +% import{1}.exclude_columns == 2 +% Default: 0. %__________________________________________________________________________ % PsPM 3.0 % (C) 2008-2015 Dominik R Bach (Wellcome Trust Centre for Neuroimaging) - -% $Id$ -% $Rev$ - -% v005 lr 23.09.2013 added support for channel names -% v004 lr 09.09.2013 removed bugs -% v003 drb 31.07.2013 changed for 3.0 architecture -% v002 drb 11.02.2011 comply with new pspm_import requirements -% v001 drb 16.9.2009 - +% (c) 2020 Ivan Rojkov (UZH) - added dsv support % initialise % ------------------------------------------------------------------------- @@ -29,44 +54,92 @@ if isempty(settings), pspm_init; end; sourceinfo = []; sts = -1; -% load & check data +% check import structure options % ------------------------------------------------------------------------- -fid = fopen(datafile); +if ~isfield(import{1}, 'delimiter') || isempty(import{1}.delimiter) + delim = 0; +elseif ~ischar(import{1}.delimiter) + warning('ID:invalid_input','The ''delimiter'' option should be a char array.') + return; +else + delim = import{1}.delimiter; +end -if isfield(import{1}, 'delimiter') - channel_names = textscan(fgetl(fid), '%s', 'Delimiter', import{1}.delimiter); +if ~isfield(import{1}, 'header_lines') + header_lines = 1; +elseif ~isnumeric(import{1}.header_lines) + warning('ID:invalid_input','The ''header_lines'' option should be a numeric value.') + return; else - channel_names = textscan(fgetl(fid), '%s'); + header_lines = import{1}.header_lines; end +if ~isfield(import{1}, 'channel_names_line') + channel_names_line = 1; + if header_lines < channel_names_line, channel_names_line=0; end +elseif ~isnumeric(import{1}.channel_names_line) + warning('ID:invalid_input','The ''channel_names_line'' option should be a numeric value.') + return; +else + channel_names_line = import{1}.channel_names_line; +end +if ~isfield(import{1}, 'exclude_columns') + exclude_columns = 0; +elseif ~isnumeric(import{1}.exclude_columns) + warning('ID:invalid_input','The ''exclude_columns'' option should be a numeric value.') + return; +else + exclude_columns = import{1}.exclude_columns; +end + +% read channel names +% ------------------------------------------------------------------------- +fid = fopen(datafile); + +% go to the specific line to read the channel names +if channel_names_line ~= 0 + for k=1:channel_names_line-1 + fgetl(fid); % read and dump + end +end + +if ischar(delim) + channel_names = textscan(fgetl(fid), '%s', 'Delimiter', delim); +else + channel_names = textscan(fgetl(fid), '%s'); +end channel_names = channel_names{1}; + fclose(fid); -fline = str2double(channel_names); -if ~any(isnan(fline)) %no headerline - if nargin == 3; - data = dlmread(datafile, delimiter); - else; - data = dlmread(datafile); - end; -elseif all(isnan(fline)) %headerline - fid = fopen(datafile); - formatSpec = repmat('%f', 1, numel(channel_names)); +% load & check data +% ------------------------------------------------------------------------- +fid = fopen(datafile); - % if delimiter provided - if isfield(import{1}, 'delimiter') - data = textscan(fid, formatSpec, 'HeaderLines', 1, 'Delimiter', import{1}.delimiter); - else; - data = textscan(fid, formatSpec, 'HeaderLines', 1); - end; - data = cell2mat(data); - fclose(fid); +formatSpec = repmat('%f', 1, numel(channel_names)); +if exclude_columns + formatSpec = repmat('%*s', 1, exclude_columns); + formatSpec = [formatSpec,repmat('%f', 1,numel(channel_names)-exclude_columns)]; +end + +if ischar(delim) + data = textscan(fid, formatSpec, 'HeaderLines', header_lines, 'Delimiter', delim); else - warning('The format of %s is not supported', datafile); return; + data = textscan(fid, formatSpec, 'HeaderLines', header_lines); end -if isempty(data), warning('An error occured while reading a textfile.\n'); return; end; +fclose(fid); + +try + data = cell2mat(data); + if isempty(data), error('The imported data are empty.'); end +catch + warning('ID:textscan_error','An error occured while reading a textfile.\n'); + return; +end + +% warning('An error occured while reading a textfile.\n'); return; end; % select desired channels % ------------------------------------------------------------------------- @@ -74,16 +147,21 @@ % define channel number if import{k}.channel > 0 chan = import{k}.channel; - else + elseif channel_names_line ~= 0 chan = pspm_find_channel(channel_names, import{k}.type); if chan < 1, return; end; - end; + else + warning('ID:invalid_input', ... + ['Neiter ''channel'' nor ''channel_names_line'' options were specified.', ... + ' Not able to import the data.']) + return; + end if chan > size(data, 2), warning('ID:channel_not_contained_in_file', 'Channel %02.0f not contained in file %s.\n', chan, datafile); return; end; import{k}.data = data(:, chan); - if strcmpi(settings.chantypes(import{k}.typeno).data, 'events') + if isfield(import{k},'typeno') && strcmpi(settings.chantypes(import{k}.typeno).data, 'events') import{k}.marker = 'continuous'; end; diff --git a/test/pspm_get_txt_test.m b/test/pspm_get_txt_test.m index 7dd3b44ad..48491cb5a 100644 --- a/test/pspm_get_txt_test.m +++ b/test/pspm_get_txt_test.m @@ -16,11 +16,11 @@ function define_testcases(this) %-------------------------------------------------------------- this.testcases{1}.pth = 'testdatafile79887.txt'; - this.testcases{1}.import{1} = struct('type', 'scr' , 'channel', 1, 'sr', 100); - this.testcases{1}.import{2} = struct('type', 'scr' , 'channel', 2, 'sr', 100); - this.testcases{1}.import{3} = struct('type', 'hr' , 'channel', 5, 'sr', 100); - this.testcases{1}.import{4} = struct('type', 'resp' , 'channel', 6, 'sr', 100); - this.testcases{1}.import{5} = struct('type', 'scr' , 'channel', 7, 'sr', 100); + this.testcases{1}.import{1} = struct('type', 'scr' , 'channel', 1, 'sr', 100, 'header_lines', 0); + this.testcases{1}.import{2} = struct('type', 'scr' , 'channel', 2, 'sr', 100, 'header_lines', 0); + this.testcases{1}.import{3} = struct('type', 'hr' , 'channel', 5, 'sr', 100, 'header_lines', 0); + this.testcases{1}.import{4} = struct('type', 'resp' , 'channel', 6, 'sr', 100, 'header_lines', 0); + this.testcases{1}.import{5} = struct('type', 'scr' , 'channel', 7, 'sr', 100, 'header_lines', 0); %generate testdata data = rand(900, 8); @@ -102,13 +102,40 @@ function del_testdata_files(this) function invalid_datafile(this) fn = 'testdatafile79887.txt'; - import{1} = struct('type', 'scr' , 'channel', 1); - import{2} = struct('type', 'scr' , 'channel', 2); - import{3} = struct('type', 'scr' , 'channel',15); + % Test wrong delimiter + import{1} = struct('type', 'scr' , 'channel', 1, 'delimiter', 24); + import = this.assign_chantype_number(import); + this.verifyWarning(@()pspm_get_txt(fn, import), 'ID:invalid_input'); + + % Test wrong header_lines + import{1} = struct('type', 'scr' , 'channel', 1, 'header_lines', 'A'); + import = this.assign_chantype_number(import); + this.verifyWarning(@()pspm_get_txt(fn, import), 'ID:invalid_input'); + + % Test wrong channel_names_line + import{1} = struct('type', 'scr' , 'channel', 1, 'channel_names_line', 'A'); + import = this.assign_chantype_number(import); + this.verifyWarning(@()pspm_get_txt(fn, import), 'ID:invalid_input'); + % Test wrong exclude_columns + import{1} = struct('type', 'scr' , 'channel', 1, 'exclude_columns', 'A'); import = this.assign_chantype_number(import); + this.verifyWarning(@()pspm_get_txt(fn, import), 'ID:invalid_input'); + % Test channel number larger than number of columns + import{1} = struct('type', 'scr' , 'channel', 1); + import{2} = struct('type', 'scr' , 'channel', 2); + import{3} = struct('type', 'scr' , 'channel',35); + import = this.assign_chantype_number(import); this.verifyWarning(@()pspm_get_txt(fn, import), 'ID:channel_not_contained_in_file'); + + % Test "no indication what to select" + import{1} = struct('type', 'scr' , 'channel', 0, 'channel_names_line', 0); + import{2} = struct('type', 'scr' , 'channel', 0, 'channel_names_line', 0); + import{3} = struct('type', 'scr' , 'channel', 0, 'channel_names_line', 0); + import = this.assign_chantype_number(import); + this.verifyWarning(@()pspm_get_txt(fn, import), 'ID:invalid_input'); + end end From a9237386edfefd7d56903613656ec5984b60b870 Mon Sep 17 00:00:00 2001 From: irojkov-ph <56234933+irojkov-ph@users.noreply.github.com> Date: Mon, 7 Sep 2020 10:41:13 +0200 Subject: [PATCH 20/31] Merge pull request #153 from bachlab/bugfix/triming_indices Bugfix/triming indices --- .gitignore | 4 ++++ doc/PsPM_Manual.lyx | 8 ++++++++ doc/release_notes.tex | 2 ++ src/pspm_trim.m | 8 ++++++-- test/pspm_trim_test.m | 6 +++--- 5 files changed, 23 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 43d172fdf..371cfa8f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +# Ignore different folders ImportTestData .bundle/ .jekyll-cache/ @@ -5,4 +6,7 @@ Gemfile Gemfile.lock _site/ vendor/ + +# Ignore all the temporary files *.lyx~ +*.m~ \ No newline at end of file diff --git a/doc/PsPM_Manual.lyx b/doc/PsPM_Manual.lyx index 0095ab07b..b01029691 100644 --- a/doc/PsPM_Manual.lyx +++ b/doc/PsPM_Manual.lyx @@ -19518,6 +19518,14 @@ endsWith from all functions for a better backward compatibility. \end_layout +\begin_layout Itemize +Fix bug in +\family typewriter +pspm_trim +\family default + which was wrongly defining the starting trimming point index. +\end_layout + \begin_layout Section PsPM Version 5.0.0 \end_layout diff --git a/doc/release_notes.tex b/doc/release_notes.tex index 4b1c91177..014fa29ad 100644 --- a/doc/release_notes.tex +++ b/doc/release_notes.tex @@ -651,6 +651,8 @@ \subsection*{Bugfixes} \item Remove \texttt{startsWith} and \texttt{endsWith} from all functions for a better backward compatibility. \end{itemize} +\item Fix bug in \texttt{pspm_trim} which was wrongly defining the starting +trimming point index. \section{PsPM Version 5.0.0} diff --git a/src/pspm_trim.m b/src/pspm_trim.m index 789d51edd..6526dec94 100644 --- a/src/pspm_trim.m +++ b/src/pspm_trim.m @@ -262,12 +262,16 @@ % trim file --- for k = 1:numel(data) if ~strcmpi(data{k}.header.units, 'events') % waveform channels - % set start and endpoint - newstartpoint = floor((sta_p + sta_offset) * data{k}.header.sr); + % set start point (`ceil` for protect against having duration < data*sr, + % the "+1" is here because of matlabs convention to start indices from 1) + newstartpoint = ceil((sta_p + sta_offset) * data{k}.header.sr)+1; if newstartpoint == 0, newstartpoint = 1; end + + % set end point newendpoint = floor((sto_p + sto_offset) * data{k}.header.sr); if newendpoint > numel(data{k}.data), ... newendpoint = numel(data{k}.data); end + % trim data data{k}.data=data{k}.data(newstartpoint:newendpoint); else % event channels diff --git a/test/pspm_trim_test.m b/test/pspm_trim_test.m index 215e78603..bc13346e5 100755 --- a/test/pspm_trim_test.m +++ b/test/pspm_trim_test.m @@ -237,7 +237,7 @@ function trimtest(testCase, datafile, reference, testnum, markerchan) nfrom = exp_val.data{filestruct.posofmarker}.data(1)+from; nto = exp_val.data{filestruct.posofmarker}.data(end)+to; - startpoint = floor(testCase.sr * nfrom); + startpoint = ceil(testCase.sr * nfrom)+1; endpoint = floor(testCase.sr * nto); for k=1:length(testCase.cont_channels) @@ -288,7 +288,7 @@ function trimtest(testCase, datafile, reference, testnum, markerchan) from = 2.1; to = exp_val.infos.duration - 2.5; - startpoint = floor(testCase.sr * from); + startpoint = ceil(testCase.sr * from)+1; endpoint = floor(testCase.sr * to); for k=1:length(testCase.cont_channels) @@ -345,7 +345,7 @@ function trimtest(testCase, datafile, reference, testnum, markerchan) nfrom = exp_val.data{filestruct.posofmarker}.data(num(1))+from; nto = exp_val.data{filestruct.posofmarker}.data(num(2))+to; - startpoint = floor(testCase.sr * nfrom); + startpoint = ceil(testCase.sr * nfrom)+1; endpoint = floor(testCase.sr * nto); for k=1:length(testCase.cont_channels) From 6de1b2586c8a36309f70defe6ba739b23892ebe1 Mon Sep 17 00:00:00 2001 From: Ivan Rojkov Date: Mon, 7 Sep 2020 10:57:49 +0200 Subject: [PATCH 21/31] Add simple_qa option into Manual --- doc/PsPM_Manual.lyx | 55 ++++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/doc/PsPM_Manual.lyx b/doc/PsPM_Manual.lyx index b01029691..d8fea21c1 100644 --- a/doc/PsPM_Manual.lyx +++ b/doc/PsPM_Manual.lyx @@ -13009,7 +13009,7 @@ Related function: \shape default \family typewriter -pspm_pp +pspm_pp, pspm_prepdata, pspm_simple_qa \end_layout \begin_layout Standard @@ -13103,6 +13103,30 @@ Maximum slope: Maximum slope in . \end_layout +\begin_layout Itemize +Missing epochs filename: Name of the .mat file where the missing epochs will + be stored. +\end_layout + +\begin_layout Itemize +Deflection threshold: Amplitude threshold which excludes epochs from the + filter if the overall deflection over this epoch is below it. + Default: 0 (no threshold). +\end_layout + +\begin_layout Itemize +Island threshold: Duration threshold (seconds) which determines the maximum + length of data between NaN epochs. + Islands of data shorter than this threshold will be removed. + Default: 0 s (no threshold). +\end_layout + +\begin_layout Itemize +Expand epochs: Duration threshold (seconds) which determines by how much + data on the flanks of artefact epochs will be removed. + Default: 0.5 s. +\end_layout + \begin_layout Subsubsection* Overwrite Existing File \end_layout @@ -19369,15 +19393,8 @@ pspm_compute_visual_angle to mm. \end_layout -\begin_layout Standard -\begin_inset Separator plain -\end_inset - - -\end_layout - \begin_layout Section -PsPM Version 4.4.0 +PsPM Version 5.0.0 \end_layout \begin_layout Subsection* @@ -19391,7 +19408,7 @@ pspm_data_editor \family default supports loading in an epoch file, and \family typewriter -pspm_pp (simple_qa) +pspm_pp (pspm_simple_qa) \family default now supports an output file for the epochs of data that is filtered out. \end_layout @@ -19415,6 +19432,11 @@ Import module of the GUI of PsPM. \end_layout +\begin_layout Itemize +Add the posibility to import DSV (delimiter separated values) as well as + CSV (comma separated values) data files. +\end_layout + \begin_layout Subsection* Changes \end_layout @@ -19526,19 +19548,6 @@ pspm_trim which was wrongly defining the starting trimming point index. \end_layout -\begin_layout Section -PsPM Version 5.0.0 -\end_layout - -\begin_layout Subsection* -New Features -\end_layout - -\begin_layout Itemize -New DSV (delimiter separated values) option for imports as well as a CSV - import function. -\end_layout - \begin_layout Part Acknowledgements \end_layout From 3c91e4917041ab52be459fec874a4ec098268c6e Mon Sep 17 00:00:00 2001 From: Ivan Rojkov Date: Mon, 7 Sep 2020 13:33:53 +0200 Subject: [PATCH 22/31] Reset release checklist --- doc/PsPM_release_checklist.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/doc/PsPM_release_checklist.md b/doc/PsPM_release_checklist.md index eb57040a9..eaea79813 100644 --- a/doc/PsPM_release_checklist.md +++ b/doc/PsPM_release_checklist.md @@ -5,20 +5,20 @@ any revisions (commits) that implement/fix something new in the release branch, don't merge these branches back to trunk. Therefore, it is sensible to create the release branch after making absolutely sure that no new stuff will be implemented. -- [x] Update version number & date in - - [x] `pspm_msg` - - [x] `pspm_quit` - - [x] `pspm.fig`: Load `pspm.fig` into MATLAB, update `fig.Children(9).String` and save back to `pspm.fig` - - [x] Manual and Developers Guide: front pages -- [x] Make sure both manuals are updated -- [x] Add release notes section of the new version to manual (at the end) and release_notes.tex -- [x] Get the manual reviewed -- [x] Create manual and dev guide PDFs using `lyx` -- [x] Check if underscores and dashes are visible in newly added manual sections -- [x] Create git branch -- [x] Delete `.asv` files if there are any (?) -- [x] Create zip of the new branch -- [x] Make sure zip doesn't contain any svn related files. As a sanity check, the zip file +- [ ] Update version number & date in + - [ ] `pspm_msg` + - [ ] `pspm_quit` + - [ ] `pspm.fig`: Load `pspm.fig` into MATLAB, update `fig.Children(9).String` and save back to `pspm.fig` + - [ ] Manual and Developers Guide: front pages +- [ ] Make sure both manuals are updated +- [ ] Add release notes section of the new version to manual (at the end) and release_notes.tex +- [ ] Get the manual reviewed +- [ ] Create manual and dev guide PDFs using `lyx` +- [ ] Check if underscores and dashes are visible in newly added manual sections +- [ ] Create git branch +- [ ] Delete `.asv` files if there are any (?) +- [ ] Create zip of the new branch +- [ ] Make sure zip doesn't contain any svn related files. As a sanity check, the zip file should be roughly the same size as the previous version zip files (maybe slightly larger but not much) - [ ] Create a release on GitHub - [ ] Upload zip to GitHub From c727bfe5b784e1aae9758134dc50005e5577b314 Mon Sep 17 00:00:00 2001 From: Ivan Rojkov Date: Mon, 7 Sep 2020 13:52:37 +0200 Subject: [PATCH 23/31] Add changes into Manual and release notes --- doc/PsPM_Manual.lyx | 41 +++++++++++++++++++++++++---------------- doc/release_notes.tex | 39 +++++++++++++++++---------------------- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/doc/PsPM_Manual.lyx b/doc/PsPM_Manual.lyx index d8fea21c1..35d981ff3 100644 --- a/doc/PsPM_Manual.lyx +++ b/doc/PsPM_Manual.lyx @@ -19402,30 +19402,39 @@ New Features \end_layout \begin_layout Itemize - +Allow \family typewriter pspm_data_editor \family default - supports loading in an epoch file, and + to load an epoch file. +\end_layout + +\begin_layout Itemize +Allow \family typewriter -pspm_pp (pspm_simple_qa) +pspm_simple_qa \family default - now supports an output file for the epochs of data that is filtered out. + to store the epochs of data that are filtered out into an output +\family typewriter +.mat +\family default + file. + The accompanying GUI editor is available under 'Artefact removal' in the + tools section. \end_layout \begin_layout Itemize - +Allow \family typewriter -pspm_convert_gaze_distance +pspm_convert_gaze_distance \family default -introduced to allow converting from distance units to degrees and scanpath - speed. - The accompanying GUI editor is availble under 'Gaze Convert' in the data - processing section. + to convert from distance units to degrees or scanpath speed. + The accompanying GUI editor is available under 'Gaze Convert' in the data + preprocessing section. \end_layout \begin_layout Itemize -Add the posibility to select the flank in the +Add the possibility to select the flank in the \family typewriter Import \family default @@ -19433,7 +19442,7 @@ Import \end_layout \begin_layout Itemize -Add the posibility to import DSV (delimiter separated values) as well as +Add the possibility to import DSV (delimiter separated values) as well as CSV (comma separated values) data files. \end_layout @@ -19442,8 +19451,8 @@ Changes \end_layout \begin_layout Itemize -The data convert functionality in tools is now split into the Gaze Convert - and Pupil Size convert in Data preprocessing. +Split the data convert functionality in tools into the 'Gaze Convert' and + 'Pupil Size convert' in the data preprocessing section. \end_layout \begin_layout Itemize @@ -19488,7 +19497,7 @@ Bugfixes \end_layout \begin_layout Itemize -DCM plot XTick is now scaled by sample rate. +Scale DCM plot XTick by sample rate. \end_layout \begin_layout Itemize @@ -19510,7 +19519,7 @@ Fix pspm_display \family default behaviour when user tries to load data with less channels than the data - he/she is curretly displaying. + he/she is currently displaying. \end_layout \begin_layout Itemize diff --git a/doc/release_notes.tex b/doc/release_notes.tex index 014fa29ad..641217f9d 100644 --- a/doc/release_notes.tex +++ b/doc/release_notes.tex @@ -607,25 +607,27 @@ \subsection*{Bugfixes} an error in the conversion factor of pixels wrt. to mm. \end{itemize} -\section{PsPM Version 4.4.0} +\section{PsPM Version 5.0.0} \subsection*{New Features} \begin{itemize} -\item \texttt{pspm\_data\_editor} supports loading in an epoch file, and -\texttt{pspm\_pp (simple\_qa)} now supports an output file for the -epochs of data that is filtered out. -\item \texttt{pspm\_convert\_gaze\_distance }introduced to allow converting -from distance units to degrees and scanpath speed. The accompanying -GUI editor is availble under 'Gaze Convert' in the data processing -section. -\item Add the posibility to select the flank in the \texttt{Import} module +\item Allow \texttt{pspm\_data\_editor} to load an epoch file. +\item Allow \texttt{pspm\_simple\_qa} to store the epochs of data that are +filtered out into an output \texttt{.mat} file. The accompanying GUI +editor is available under 'Artefact removal' in the tools section. +\item Allow \texttt{pspm\_convert\_gaze\_distance} to convert from distance +units to degrees or scanpath speed. The accompanying GUI editor is +available under 'Gaze Convert' in the data preprocessing section. +\item Add the possibility to select the flank in the \texttt{Import} module of the GUI of PsPM. +\item Add the possibility to import DSV (delimiter separated values) as +well as CSV (comma separated values) data files. \end{itemize} \subsection*{Changes} \begin{itemize} -\item The data convert functionality in tools is now split into the Gaze -Convert and Pupil Size convert in Data preprocessing. +\item Split the data convert functionality in tools into the 'Gaze Convert' +and 'Pupil Size convert' in the data preprocessing section. \item Factor out blink/saccade edge filtering logic out of \texttt{pspm\_get\_eyelink} to \texttt{pspm\_blink\_saccade\_filt}. \item Deprecate edge filtering functionality in \texttt{pspm\_get\_eyelink}. @@ -637,12 +639,12 @@ \subsection*{Changes} \subsection*{Bugfixes} \begin{itemize} -\item DCM plot XTick is now scaled by sample rate. +\item Scale DCM plot XTick by sample rate. \item Correct index offset when dealing with the descending flank for continuous markers. \item Allow \texttt{pspm\_display} to plot any type of marker channels. \item Fix \texttt{pspm\_display} behaviour when user tries to load data -with less channels than the data he/she is curretly displaying. +with less channels than the data he/she is currently displaying. \item Fix \texttt{pspm\_split\_sessions} behaviour when the intertrial interval in the data is random. Add an option \texttt{randomITI} (0 or 1) which in the latter case it forces the function to evaluate the mean marker @@ -650,18 +652,11 @@ \subsection*{Bugfixes} 0. \item Remove \texttt{startsWith} and \texttt{endsWith} from all functions for a better backward compatibility. -\end{itemize} -\item Fix bug in \texttt{pspm_trim} which was wrongly defining the starting +\item Fix bug in \texttt{pspm\_trim} which was wrongly defining the starting trimming point index. - - -\section{PsPM Version 5.0.0} - -\subsection*{New Features} -\begin{itemize} -\item New DSV (delimiter separated values) option for imports as well as a CSV import function \end{itemize} + \section{References} \bibliographystyle{pnas2009} From 3f6767bc2bd1e88e03bd78e20272ed472651b1e9 Mon Sep 17 00:00:00 2001 From: Ivan Rojkov Date: Mon, 7 Sep 2020 14:00:18 +0200 Subject: [PATCH 24/31] Modify pspm init message --- src/pspm_msg.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pspm_msg.txt b/src/pspm_msg.txt index 3422fc386..9d2f87a40 100644 --- a/src/pspm_msg.txt +++ b/src/pspm_msg.txt @@ -1,7 +1,7 @@ $___________________________________________________________________________ Welcome to PsPM - PsychoPhysiological Modelling (incorporating SCRalyze) -Version 4.4.0 (21.08.2020) +Version 5.0.0 (21.08.2020) $ ------------------------------------------ (c) 2008-2020 From c0cb5774d62eb91c07ae4231eacc0699a39230e5 Mon Sep 17 00:00:00 2001 From: Ivan Rojkov Date: Mon, 7 Sep 2020 14:00:32 +0200 Subject: [PATCH 25/31] Modify PsPM figure --- src/pspm.fig | Bin 25489 -> 26883 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/pspm.fig b/src/pspm.fig index 19cd46d905c3d2308185a9df6e3e7fdef7a87732..a58c1a6e6bf14ea19c44478efb294911ee8a0e9b 100644 GIT binary patch literal 26883 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 literal 25489 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 z ze*pjh|Nrb)Pfrs;6rWOB;egh}7!D@%fB_Oxnx5dqR80^fit^{6hi2Mt>B4k&vYmzW zMl?}~|4!VEiHRP#dGKQpgef{f_>NLR24*X|Mwbz_{AGRo#U1|Q0Fg%1ZdS4z z@UWBDl`uWaxZmrrMfPXM(XHKV8@m{2{kOIq$J8idwG_U6(!*Zf?_tM!e$8`x=rjKn z*ZdskNI8v_9R;UrT3Nx(I97b(XjeC%4Q2!7QdX=?)csFC9MoN;da${usBl!Lvz#+!>Fih8nb?{D-`L7 zX*#emVJa~bM@YpWC8lbmP(`GSl~eiTm6v0{nU@^Tt)nvihL7rIgsT*#HPDBZj7Eoz^BNN*c)(ZX=9~ zsI)yXed@L8O+)3{b}%*7N&gvk#lbNm8fnxtqb{EJ(!ax~@8_UZ-{%oW!Xu7rQXIC# z&+zB9ucNLwI5y8yvnD1#D%|R<~BL3%9AK&}L|7gVjLg9at7nJ|)`>bcxwZ9y<=YPBtEUYgEqkzDU z2l~YGKk&d;;NtN>zw<=yBPoFwG_720D4FP9=drE;=T*0P^cr~I>pDIEQcd|eZ|UQC zJMu8{LGBx2@J7FQF1h02Sah1M?@PME44s5mZ!-Tp@AX7Hv?-2yc>^LI`Y-#{TM+Tk zh=+#DLoY&K)^-#fqXzf`00960>{v~2+(Z~2(sa{AX;`GtRurUyR4Nc;sZ^vQ6m>qUCZAwxX9D5{*`z>@Bcg>1BX-VAkM!tTU882Up zTD}7SPoq!=7eu1hiR2&{+$SQjDM?=@*Nh6;@%w}8hz6LGxC#S5hysuBNaYf{)Yppa z^<~Fp`!}DCw^@yUxg!5WMgA)l`Sl~_FIVJOoxkDzeLVi_O?zEs?a#9YzYY@jIVt;I zNp^>7em}bMf8d*Wo1YoYpV3#7nC~4lI`8LEI`2wuYzq>KhQOYO`=1iV8j-^qZPDIr zh|p;SBJftcg8DC5`Y#mJuXg<+bu(w_S8}`$Up$iY(SPYra^E~Zw9)ALFxR81k8|iT z?ej02asN%Tn%+OI#Qkin51rHfnPy)6wT$@BSn(@-4_HLQ=fx~P&Acf;)tt1R`|yoF z|GCtB{`bFLW529cjQ6lTxF4)8{P=eBlhuU>_aD7Gv3P&bTw7mO{hdPT`cra?OA>O- z)=3vfOtwQm*u=r`IVnruv6Q}J2=o1+bx`J<|0Lr`nyD}Gqbx6PAE;S8B?nQ5QD^e} zGVh~F)aA0yg+gHHTeS4mkzEh*x~v!(S9#=-5UT~z z;zDBX5cSh?jbR}^(Esob+bzr@qHc@f+v!^ew_Dt$PHOPIh%!lewh4Pa?DLQvPc6>e z!!o>DNqgrtua6#o-m!U;I_I6$VI*>9$A(283z9xgAXZ?&1wPorXpp%LNaivTa1Sfc7G5YHMCatEXC^1+=(!}8Bx3U) zClji9FaR?TW*-lY=ft8vf&n$%M1T$S Z^o8m7AYebf^y};F9}F`G1OOmYxicknqEY|= From 094a470b7a96f70b67a522598667473a2aa448ec Mon Sep 17 00:00:00 2001 From: Ivan Rojkov Date: Mon, 7 Sep 2020 14:02:27 +0200 Subject: [PATCH 26/31] Modify version number pspm_quit --- src/pspm_quit.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pspm_quit.m b/src/pspm_quit.m index 90ffb98d8..b67fc936f 100644 --- a/src/pspm_quit.m +++ b/src/pspm_quit.m @@ -1,6 +1,6 @@ % pspm_quit clears settings, removes paths & closes figures %__________________________________________________________________________ -% PsPM 4.4.0 +% PsPM 5.0.0 % (C) 2008-2019 Dominik R Bach (Wellcome Trust Centre for Neuroimaging) % % $Id: pspm_quit.m 805 2019-09-16 07:12:08Z esrefo $ From 0852ba5d7c7490cf74bd0d701c58748cc874fc6a Mon Sep 17 00:00:00 2001 From: Ivan Rojkov Date: Mon, 7 Sep 2020 14:03:47 +0200 Subject: [PATCH 27/31] Modify version number in manuals --- doc/PsPM_Developers_Guide.lyx | 2 +- doc/PsPM_Manual.lyx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/PsPM_Developers_Guide.lyx b/doc/PsPM_Developers_Guide.lyx index 15dfc4731..52ac602cf 100644 --- a/doc/PsPM_Developers_Guide.lyx +++ b/doc/PsPM_Developers_Guide.lyx @@ -98,7 +98,7 @@ Developer's Guide \begin_layout Standard \align center -Version 4.4.0 +Version 5.0.0 \end_layout \begin_layout Standard diff --git a/doc/PsPM_Manual.lyx b/doc/PsPM_Manual.lyx index 35d981ff3..2235caeb6 100644 --- a/doc/PsPM_Manual.lyx +++ b/doc/PsPM_Manual.lyx @@ -103,7 +103,7 @@ PsPM: Psychophysiological Modelling \begin_layout Standard \align center -Version 4.4.0 +Version 5.0.0 \end_layout \begin_layout Standard From afce2cf67b73e445da34cf9d278848e99d27fbc4 Mon Sep 17 00:00:00 2001 From: Ivan Rojkov Date: Mon, 7 Sep 2020 14:04:28 +0200 Subject: [PATCH 28/31] Minor change --- doc/PsPM_release_checklist.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/PsPM_release_checklist.md b/doc/PsPM_release_checklist.md index eaea79813..c3ef11fdf 100644 --- a/doc/PsPM_release_checklist.md +++ b/doc/PsPM_release_checklist.md @@ -8,7 +8,7 @@ release branch after making absolutely sure that no new stuff will be implemente - [ ] Update version number & date in - [ ] `pspm_msg` - [ ] `pspm_quit` - - [ ] `pspm.fig`: Load `pspm.fig` into MATLAB, update `fig.Children(9).String` and save back to `pspm.fig` + - [ ] `pspm.fig`: Load `pspm.fig` into MATLAB using `openfig`, update `fig.Children(9).String` and save back to `pspm.fig` - [ ] Manual and Developers Guide: front pages - [ ] Make sure both manuals are updated - [ ] Add release notes section of the new version to manual (at the end) and release_notes.tex From d16e8513807047aeb70cde941a115a266382bf7d Mon Sep 17 00:00:00 2001 From: irojkov-ph <56234933+irojkov-ph@users.noreply.github.com> Date: Mon, 7 Sep 2020 15:47:44 +0200 Subject: [PATCH 29/31] Merge pull request #146 from bachlab/fix/epoch-output-128 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(filter-epochs): include slope threshold and use array ops instea… --- doc/PsPM_Manual.lyx | 2 +- src/pspm_cfg/pspm_cfg_artefact_rm.m | 85 ++++++++++++--- src/pspm_cfg/pspm_cfg_run_artefact_rm.m | 43 +++++++- src/pspm_pp.m | 7 ++ src/pspm_simple_qa.m | 132 +++++++++++++++++++----- test/pspm_pp_test.m | 4 +- 6 files changed, 228 insertions(+), 45 deletions(-) diff --git a/doc/PsPM_Manual.lyx b/doc/PsPM_Manual.lyx index 2235caeb6..8a982140d 100644 --- a/doc/PsPM_Manual.lyx +++ b/doc/PsPM_Manual.lyx @@ -13111,7 +13111,7 @@ Missing epochs filename: Name of the .mat file where the missing epochs will \begin_layout Itemize Deflection threshold: Amplitude threshold which excludes epochs from the filter if the overall deflection over this epoch is below it. - Default: 0 (no threshold). + Default: 0.1 . \end_layout \begin_layout Itemize diff --git a/src/pspm_cfg/pspm_cfg_artefact_rm.m b/src/pspm_cfg/pspm_cfg_artefact_rm.m index c5cfb7619..76c2e9b56 100644 --- a/src/pspm_cfg/pspm_cfg_artefact_rm.m +++ b/src/pspm_cfg/pspm_cfg_artefact_rm.m @@ -75,27 +75,80 @@ qa_missing_epochs_no_filename.val = {0}; qa_missing_epochs_no_filename.help = {'Do not store artefacts epochs to file'}; -qa_missing_epochs_filename_path = cfg_entry; -qa_missing_epochs_filename_path.name = 'Write to filename'; -qa_missing_epochs_filename_path.tag = 'missing_epochs_filename_path'; -qa_missing_epochs_filename_path.strtype = 's'; -qa_missing_epochs_filename_path.num = [ 1 Inf ]; -qa_missing_epochs_filename_path.help = {'Filename to store artefact epochs. Provide only the name and not extension, the file will be stored as a .mat file'}; - -qa_missing_epochs_filename = cfg_choice; -qa_missing_epochs_filename.name = 'Missing epochs file'; -qa_missing_epochs_filename.tag = 'missing_epochs'; -qa_missing_epochs_filename.val = {qa_missing_epochs_no_filename}; -qa_missing_epochs_filename.values = {qa_missing_epochs_no_filename, qa_missing_epochs_filename_path}; -qa_missing_epochs_filename.help = {'Artefact epochs file behaviour'}; +qa_missing_epochs_file_name = cfg_entry; +qa_missing_epochs_file_name.name = 'File name'; +qa_missing_epochs_file_name.tag = 'filename'; +qa_missing_epochs_file_name.strtype = 's'; +qa_missing_epochs_file_name.num = [ 1 Inf ]; +qa_missing_epochs_file_name.help = {['Specify the name of the file where to store artefact epochs. ',... + 'Provide only the name and not the extension, the file will be stored as a .mat file']}; + +qa_missing_epochs_file_path = cfg_files; +qa_missing_epochs_file_path.name = 'Output Directory'; +qa_missing_epochs_file_path.tag = 'outdir'; +qa_missing_epochs_file_path.filter = 'dir'; +qa_missing_epochs_file_path.num = [1 1]; +qa_missing_epochs_file_path.help = {'Specify the directory where the .mat file with artefact epochs will be written.'}; + +qa_missing_epochs_file = cfg_exbranch; +qa_missing_epochs_file.name = 'Write to filename'; +qa_missing_epochs_file.tag = 'write_to_file'; +qa_missing_epochs_file.val = {qa_missing_epochs_file_name, qa_missing_epochs_file_path}; +qa_missing_epochs_file.help = {['If you choose to store the artefact epochs please specify a filename ',... + 'as well as an output directory. When giving the filename do not specify ',... + 'any extension, the artefact epochs will be stored as .mat file.']}; + +qa_missing_epochs = cfg_choice; +qa_missing_epochs.name = 'Missing epochs file'; +qa_missing_epochs.tag = 'missing_epochs'; +qa_missing_epochs.val = {qa_missing_epochs_no_filename}; +qa_missing_epochs.values = {qa_missing_epochs_no_filename, qa_missing_epochs_file}; +qa_missing_epochs.help = {'Specify if you want to store the artefact epochs in a separate file of not.', ... + 'Default: artefact epochs are not stored.'}; + +qa_deflection_threshold = cfg_entry; +qa_deflection_threshold.name = 'Deflection threshold'; +qa_deflection_threshold.tag = 'deflection_threshold'; +qa_deflection_threshold.strtype = 'r'; +qa_deflection_threshold.num = [1 1]; +qa_deflection_threshold.val = {0.1}; +qa_deflection_threshold.help = {['Define an threshold in original data units for a slope to pass to be considerd in the filter. ', ... + 'This is useful, for example, with oscillatory wave data. ', ... + 'The slope may be steep due to a jump between voltages but we ', ... + 'likely do not want to consider this to be filtered. ', ... + 'A value of 0.1 would filter oscillatory behaviour with threshold less than 0.1v but not greater.' ],... + 'Default: 0.1', ... + }; + +qa_data_island_threshold = cfg_entry; +qa_data_island_threshold.name = 'Data island threshold'; +qa_data_island_threshold.tag = 'data_island_threshold'; +qa_data_island_threshold.strtype = 'r'; +qa_data_island_threshold.num = [1 1]; +qa_data_island_threshold.val = {0}; +qa_data_island_threshold.help = {['A float in seconds to determine the maximum length of unfiltered data between epochs.', ... + ' If an island exists for less than the threshold it will also be filtered'], ... + 'Default: 0 s - will take no effect on filter', ... + }; + +qa_expand_epochs = cfg_entry; +qa_expand_epochs.name = 'Expand epochs'; +qa_expand_epochs.tag = 'expand_epochs'; +qa_expand_epochs.strtype = 'r'; +qa_expand_epochs.num = [1 1]; +qa_expand_epochs.val = {0.5}; +qa_expand_epochs.help = {'A float in seconds to determine by how much data on the flanks of artefact epochs will be removed.', ... + 'Default: 0.5 s', ... + }; + qa = cfg_branch; qa.name = 'Simple SCR quality correction'; qa.tag = 'simple_qa'; -qa.val = {qa_min, qa_max, qa_slope, qa_missing_epochs_filename}; +qa.val = {qa_min, qa_max, qa_slope, qa_missing_epochs, qa_deflection_threshold, qa_data_island_threshold,qa_expand_epochs}; qa.help = {['Simple SCR quality correction. See I. R. Kleckner et al.,"Simple, Transparent, and' ... - 'Flexible Automated Quality Assessment Procedures for Ambulatory Electrodermal Activity Data," in ' ... - 'IEEE Transactions on Biomedical Engineering, vol. 65, no. 7, pp. 1460-1467, July 2018.']}; + 'Flexible Automated Quality Assessment Procedures for Ambulatory Electrodermal Activity Data," in ' ... + 'IEEE Transactions on Biomedical Engineering, vol. 65, no. 7, pp. 1460-1467, July 2018.']}; %% Data file datafile = cfg_files; diff --git a/src/pspm_cfg/pspm_cfg_run_artefact_rm.m b/src/pspm_cfg/pspm_cfg_run_artefact_rm.m index b7fcf7064..ed4f1a5e5 100644 --- a/src/pspm_cfg/pspm_cfg_run_artefact_rm.m +++ b/src/pspm_cfg/pspm_cfg_run_artefact_rm.m @@ -21,10 +21,47 @@ freq = job.filtertype.(filtertype).freq; out = pspm_pp(filtertype, datafile, freq, channelnumber, options); case 'simple_qa' - qa = job.filtertype.(filtertype); - if isfield(qa.missing_epochs, 'missing_epochs_filename_path') - qa.missing_epochs_filename = qa.missing_epochs.missing_epochs_filename_path; + qa_job = job.filtertype.(filtertype); + + % Option structure sent to pspm_simple_qa + qa = struct(); + + % Check if min is defined + if isfield(qa_job, 'min'), qa.min = qa_job.min; end + + % Check if max is defined + if isfield(qa_job, 'max'), qa.max = qa_job.max; end + + % Check if slope is defined + if isfield(qa_job, 'slope'), qa.slope = qa_job.slope; end + + % Check if missing_epochs is defined + if isfield(qa_job.missing_epochs, 'write_to_file') + if isfield(qa_job.missing_epochs.write_to_file,'filename') && ... + isfield(qa_job.missing_epochs.write_to_file,'outdir') + + qa.missing_epochs_filename = fullfile( ... + qa_job.missing_epochs.write_to_file.outdir{1}, ... + qa_job.missing_epochs.write_to_file.filename); + + end + end + + % Check if deflection_threshold is defined + if isfield(qa_job, 'deflection_threshold') + qa.deflection_threshold = qa_job.deflection_threshold; + end + + % Check if data_island_threshold is defined + if isfield(qa_job, 'data_island_threshold') + qa.data_island_threshold = qa_job.data_island_threshold; + end + + % Check if expand_epochs is defined + if isfield(qa_job, 'expand_epochs') + qa.expand_epochs = qa_job.expand_epochs; end + out = pspm_pp(filtertype, datafile, qa, channelnumber, options); end diff --git a/src/pspm_pp.m b/src/pspm_pp.m index c7baa7a4d..6565c5561 100644 --- a/src/pspm_pp.m +++ b/src/pspm_pp.m @@ -19,6 +19,13 @@ % slope: Maximum slope in microsiemens per second % missing_epochs_filename: If provided will create a .mat file with the missing epochs, % e.g. abc will create abc.mat +% deflection_threshold: Define an threshold in original data units for a slope to pass to be considerd in the filter. +% This is useful, for example, with oscillatory wave data +% The slope may be steep due to a jump between voltages but we +% likely do not want to consider this to be filtered. +% A value of 0.1 would filter oscillatory behaviour with threshold less than 0.1v but not greater +% Default: 0 - ie will take no effect on filter +% data_island_threshold: A float in seconds to determine the maximum length of unfiltered data between epochs %__________________________________________________________________________ % % References: For 'simple_qa' method, refer to: diff --git a/src/pspm_simple_qa.m b/src/pspm_simple_qa.m index f9ce135cd..6b4bcab11 100644 --- a/src/pspm_simple_qa.m +++ b/src/pspm_simple_qa.m @@ -18,9 +18,23 @@ % slope: Maximum slope in microsiemens per sec (default: 10). % missing_epochs_filename: If provided will create a .mat file with the missing epochs, % e.g. abc will create abc.mat +% deflection_threshold: Define an threshold in original data units for a slope to pass to be considerd in the filter. +% This is useful, for example, with oscillatory wave data due to limited A/D bandwidth +% The slope may be steep due to a jump between voltages but we +% likely do not want to consider this to be filtered. +% A value of 0.1 would filter oscillatory behaviour with threshold less than 0.1v but not greater +% Default: 0.1 +% data_island_threshold: A float in seconds to determine the maximum length of data between NaN epochs. Islands of data +% shorter than this threshold will be removed. +% Default: 0 s - no effect on filter +% expand_epochs: A float in seconds to determine by how much data on the flanks of artefact epochs will be removed. +% Default: 0.5 s +% +% %__________________________________________________________________________ -% PsPM 3.2 -% (C) 2009-2017 Tobias Moser (University of Zurich) +% PsPM 5.0 +% 2009-2017 Tobias Moser (University of Zurich) +% 2020 Samuel Maxwell & Dominik Bach (UCL) % $Id: pspm_pp.m 450 2017-07-03 15:17:02Z tmoser $ % $Rev: 450 $ @@ -50,6 +64,18 @@ options.slope = 10; end +if ~isfield(options, 'deflection_threshold') + options.deflection_threshold = 0.1; +end + +if ~isfield(options, 'data_island_threshold') + options.data_island_threshold = nan; +end + +if ~isfield(options, 'expand_epochs') + options.expand_epochs = 0.5; +end + % sanity checks if ~isnumeric(data) warning('ID:invalid_input', 'Argument ''data'' must be numeric.'); return; @@ -63,23 +89,84 @@ warning('ID:invalid_input', 'Argument ''options.max'' must be numeric.'); return; elseif ~isnumeric(options.slope) warning('ID:invalid_input', 'Argument ''options.slope'' must be numeric.'); return; -elseif isfield(options, 'missing_epochs_filename') && ~ischar(options.missing_epochs_filename) - warning('ID:invalid_input', 'Argument ''options.missing_epochs_filename'' must be char array.'); return; +elseif isfield(options, 'missing_epochs_filename') + if ~ischar(options.missing_epochs_filename) + warning('ID:invalid_input', 'Argument ''options.missing_epochs_filename'' must be char array.'); return; + end + + [pth, ~, ext] = fileparts(options.missing_epochs_filename); + if ~isempty(pth) && exist(pth,'dir')~=7 + warning('ID:invalid_input','Please specify a valid output directory if you want to save artefact epochs.') + return; + end + if ~isempty(ext) + warning('ID:invalid_input','Please specify a valid filename (without extension) if you want to save artefact epochs.') + return; + end end % create filters d = NaN(size(data)); range_filter = data < options.max & data > options.min; slope_filter = true(size(data)); -slope_filter(2:end) = abs(diff(data)*sr) < options.slope; +diff_data = diff(data); +slope_filter(2:end) = abs(diff_data*sr) < options.slope; + +if (options.deflection_threshold ~= 0) && ~all(slope_filter==1) + + slope_epochs = filter_to_epochs(slope_filter); + for r = slope_epochs' + if range(data(r(1):r(2))) < options.deflection_threshold + slope_filter(r(1):r(2)) = 1; + end + end + +end % combine filters filt = range_filter & slope_filter; + +% find data islands and expand artefact islands +if options.data_island_threshold > 0 || options.expand_epochs > 0 + + % work out data epochs + data_epochs = filter_to_epochs(1-filt); % gives data (rather than artefact) epochs + + if options.expand_epochs > 0 + % remove data epochs too short to be shortened + epoch_duration = diff(data_epochs, 1, 2); + data_epochs(epoch_duration < 2 * ceil(options.expand_epochs * sr), :) = []; + % shorten data epochs + data_epochs(:, 1) = data_epochs(:, 1) + ceil(options.expand_epochs * sr); + data_epochs(:, 2) = data_epochs(:, 2) - ceil(options.expand_epochs * sr); + end + + % correct possibly negative values + data_epochs(data_epochs(:, 2) < 1, 2) = 1; + + if options.data_island_threshold > 0 + epoch_duration = diff(data_epochs, 1, 2); + data_epochs(epoch_duration < options.data_island_threshold * sr, :) = []; + end + + + % write back into data + index(data_epochs(:, 1)) = 1; + index(data_epochs(:, 2)) = -1; + filt = (cumsum(index(:)) == 1); % (thanks Jan: https://www.mathworks.com/matlabcentral/answers/324955-replace-multiple-intervals-in-array-with-nan-no-loops) +end + + d(filt) = data(filt); % write epochs to mat if missing_epochs_filename option is present if isfield(options, 'missing_epochs_filename') - epochs = collect_epochs(filt, sr); + if ~isempty(find(filt == 0, 1)) + epochs = filter_to_epochs(filt); + else + epochs = []; + end + save(options.missing_epochs_filename, 'epochs'); end @@ -88,22 +175,19 @@ end -% construct epochs using filt 0 as offset and 1 as onset -function epochs = collect_epochs(filt, sr) - epochs = []; - epoch_start = NaN; - for i = 1:numel(filt) - if ~filt(i) && isnan(epoch_start) - epoch_start = (i - 1) / sr; - end - if (filt(i) && ~isnan(epoch_start)) - new_epoch = [ epoch_start, (i - 1) / sr ]; - epochs = [ epochs; new_epoch ]; - epoch_start = NaN; - end - end - if ~isnan(epoch_start) - new_epoch = [ epoch_start, numel(filt) / sr ]; - epochs = [ epochs; new_epoch ]; - end +function epochs = filter_to_epochs(filt) +epoch_on = find(diff(filt) == -1) + 1; +epoch_off = find(diff(filt) == 1); + +% ends on +if (epoch_on(end) > epoch_off(end)) + epoch_off(end + 1) = length(filt); +end + +% starts on +if (epoch_on(1) > epoch_off(1)) + epoch_on = [ 1; epoch_on ]; +end + +epochs = [ epoch_on, epoch_off ]; end \ No newline at end of file diff --git a/test/pspm_pp_test.m b/test/pspm_pp_test.m index 55267d1da..407efd6ef 100644 --- a/test/pspm_pp_test.m +++ b/test/pspm_pp_test.m @@ -102,7 +102,9 @@ function simple_qa_test(this) %filter one channel missing_epoch_filename = 'missing_epochs_test_out'; - qa = struct('missing_epochs_filename', missing_epoch_filename); + qa = struct('missing_epochs_filename', missing_epoch_filename, ... + 'deflection_threshold', 0, ... + 'expand_epochs', 0 ); newfile = pspm_pp('simple_qa', fn, qa); [sts, infos, data, filestruct] = pspm_load_data(newfile, 'none'); From acba21dbd743756fff11cf688192590b47845dab Mon Sep 17 00:00:00 2001 From: Ivan Rojkov Date: Tue, 8 Sep 2020 08:38:29 +0200 Subject: [PATCH 30/31] Fix typos in docs spotted by DB --- doc/PsPM_Manual.lyx | 10 ++++++++++ doc/release_notes.tex | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/doc/PsPM_Manual.lyx b/doc/PsPM_Manual.lyx index 8a982140d..8d90ff0c8 100644 --- a/doc/PsPM_Manual.lyx +++ b/doc/PsPM_Manual.lyx @@ -19409,6 +19409,16 @@ pspm_data_editor to load an epoch file. \end_layout +\begin_layout Itemize +Allow +\family typewriter +pspm_simple_qa +\family default + to suppress classification of discretisation oscillations as artefacts, + to expand artefact windows, and to automatically remove small data islands + embedded in artefacts. +\end_layout + \begin_layout Itemize Allow \family typewriter diff --git a/doc/release_notes.tex b/doc/release_notes.tex index 641217f9d..f6739ccbc 100644 --- a/doc/release_notes.tex +++ b/doc/release_notes.tex @@ -65,7 +65,7 @@ framexleftmargin=1pt, frame=l} \renewcommand{\lstlistingname}{Listing} -\title{PsPM 4.3.0 Release notes} +\title{PsPM 5.0.0 Release notes} \begin{document} \maketitle @@ -612,6 +612,7 @@ \section{PsPM Version 5.0.0} \subsection*{New Features} \begin{itemize} \item Allow \texttt{pspm\_data\_editor} to load an epoch file. +\item Allow \texttt{pspm\_simple\_qa} to suppress classification of discretisation oscillations as artefacts, to expand artefact windows, and to automatically remove small data islands embedded in artefacts. \item Allow \texttt{pspm\_simple\_qa} to store the epochs of data that are filtered out into an output \texttt{.mat} file. The accompanying GUI editor is available under 'Artefact removal' in the tools section. From b332126f10d834c7302f7169e971ff498a8d7d91 Mon Sep 17 00:00:00 2001 From: Dominik Bach Date: Tue, 8 Sep 2020 19:58:38 +0100 Subject: [PATCH 31/31] update pspm_quit --- src/pspm_quit.m | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/pspm_quit.m b/src/pspm_quit.m index b67fc936f..7b2911b91 100644 --- a/src/pspm_quit.m +++ b/src/pspm_quit.m @@ -1,21 +1,11 @@ % pspm_quit clears settings, removes paths & closes figures %__________________________________________________________________________ % PsPM 5.0.0 -% (C) 2008-2019 Dominik R Bach (Wellcome Trust Centre for Neuroimaging) +% (C) 2008-2020 Dominik R Bach (Wellcome Trust Centre for Neuroimaging) % % $Id: pspm_quit.m 805 2019-09-16 07:12:08Z esrefo $ % $Rev: 805 $ % -% v109 drb 14.08.2013 removed import paths -% v108 30.05.2013 removed fil distribution -% v107 17.05.2013 updated footer -% v106 08.05.2012 updated footer & added more import paths -% v105 20.07.2011 updated footer -% v104 7.9.2010 changed import path structure -% v103 drb 2.9.2010 made compatible with other OSs (filesep), added vario -% v102 drb 20.3.2010 added DAVB & acq path -% v101 drb 8.9.2009 - global settings if isempty(settings), pspm_init; end; fs = filesep; @@ -33,6 +23,6 @@ disp(' '); disp('Thanks for using PsPM.'); disp('_____________________________________________________________________________________________'); -disp('PsPM 4.3.0 (c) 2008-2019 Dominik R. Bach'); +disp('PsPM 5.0.0 (c) 2008-2020 Dominik R. Bach'); disp('University of Zurich, CH -- University College London, UK');