From 7b730160cbd8cc25f3ea9a19cac07ffd95f911f9 Mon Sep 17 00:00:00 2001 From: Dominik Bach Date: Fri, 5 Dec 2025 15:57:59 +0100 Subject: [PATCH 1/8] new pipeline for fear-conditioned SCR --- src/pspm_pipeline_fc_scr.m | 122 +++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 src/pspm_pipeline_fc_scr.m diff --git a/src/pspm_pipeline_fc_scr.m b/src/pspm_pipeline_fc_scr.m new file mode 100644 index 00000000..9f7bfa8f --- /dev/null +++ b/src/pspm_pipeline_fc_scr.m @@ -0,0 +1,122 @@ +function [sts, out] = pspm_pipeline_fc_scr(fn, missing, onsets, isi, method, normalize, keepfile) +% ● Description +% This function runs a standard PsPM pipeline for fear-conditioned SCR, +% based on the winning methods from the paper de Vries et al. (2026) in +% preparation. +% ● Format +% [sts, out] = pspm_pipeline_fc_scr(fn, missing, onsets, soa, method, norm, keepfile) +% ● Arguments +% * fn: a PsPM data file name, or cell array of file names +% * missing: a PsPM epoch file name (set to [] if no missing file), or cell +% array of epoch file names +% * onsets: onset times of all CS (vector), or cell array of onset times +% * isi: CS-US interval in s (scalar or vector, in the latter case must be +% defined for all trials, or cell array) +% * method: one of '2026_short_uni', '2026_short_bi', '2026_long_uni', +% '2026_short_bi'. Here, 'long/short' refer to the isi in de Vries +% et al. (which was 3.5 s for 'short', and > 6 s for 'long'), and +% 'uni/bi' refers to unidirectional or bidirectional filtering +% (where bidirectional filtering was slightly better in the tested +% data sets but can be suboptimal with long inter-trial intervals) +% * normalize: [optional] Normalise data. Data are normalised during inversion +% but results are transformed back into raw data units. Default: 0. +% * keepfile: [optional] Save model file for diagnostic purposes. Default: 0. +% +% ● Outputs +% * out: trial-by-trial estimate of the conditioned response. +% ● History +% Introduced in PsPM 7.2 +% ● References +% [1] de Vries et al. (2026) forthcoming. + +%% Initialise. +% most of the checks are performed in downstream functions and give +% expressive warnings +global settings +if isempty(settings) + pspm_init; +end +sts = -1; +out = []; + +if nargin <= 5 + warning('Don''t know what to do'); + return +end + +if nargin < 6 + normalize = 0; +end + +if nargin < 7 + keepfile = 0; +end + +%% Parse options and setup timings +if ~iscell(fn) + onsets = {onsets}; + isi = {isi}; +end + +for i_sn = 1:numel(onsets) + timing{i_sn}{1} = onsets{i_sn}(:) + isi{i_sn};(:) + if ismember(method, {'2026_short_uni', '2026_short_bi'}) + % flex-fix + timing{i_sn}{2} = [onsets{i_sn}(:); onsets{i_sn}(:) + isi{i_sn}(:)]; + elseif ismember(method, {'2026_long_uni', '2026_long_bi'}) + % flex-flex-fix with halved ISI + timing{i_sn}{2} = [onsets{i_sn}(:); onsets{i_sn}(:) + isi{i_sn}(:)/2]; + timing{i_sn}{3} = [onsets{i_sn}(:) + isi{i_sn}(:)/2; onsets{i_sn}(:) + isi{i_sn}(:)]; + else + warning('Unknown method'); + return + end +end + +if ~iscell(fn) + timing = timing{1}; +end + +%% Setup model +model = struct( ... + datafile, fn, ... + missing, missing, ... + timing, timing, ... + norm, normalize); +% set (dummy) filename +[pth, fn, ext] = fileparts(fn); +model_fn = fullfile(pth, ['mdl_', fn, ext]); +model.modelfile = model_fn; + +if strcmpi(method, '2026_short_uni') +elseif strcmpi(method, '2026_short_bi') +elseif strcmpi(method, '2026_long_uni') + % this is a hidden option in pspm_dcm_inv (upper bound in s) + model.constrained_upper = 1; + % filter + model.filter = settings.dcm{1}.filter; + model.filter.direction = 'uni'; +elseif strcmpi(method, '2026_long_bi') + % default model +end + +%% Setup options +options = struct('overwrite', 0, 'nosave', 1-keepfile); + +%% Run DCM +[sts, dcm] = pspm_dcm(model, options); + +%% Construct readout for short and long SOA +if sts > 0 + % find amplitude and dispersion estimates + amp_indx = find(contains(dcm.names, 'amplitude')); + disp_indx = find(contains(dcm.names, 'dispersion')); + + if startsWith(method, '2026_short') + out = dcm.stats(:, amp_indx); + elseif startsWith(method, '2026_long') + out = dcm.stats(:, amp_indx(1)) .* dcm.stats(:, disp_indx(1)) + ... % a x c for each response, then summed + dcm.stats(:, amp_indx(2)) .* dcm.stats(:, disp_indx(2)); + end +end + From 27ac6bc27b630d37d119687bf65f99727fd088f4 Mon Sep 17 00:00:00 2001 From: 4gwe Date: Mon, 8 Dec 2025 00:59:30 +0100 Subject: [PATCH 2/8] updates and improvements --- src/pspm_pipeline_fc_scr.m | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/pspm_pipeline_fc_scr.m b/src/pspm_pipeline_fc_scr.m index 9f7bfa8f..ed848721 100644 --- a/src/pspm_pipeline_fc_scr.m +++ b/src/pspm_pipeline_fc_scr.m @@ -13,7 +13,7 @@ % * isi: CS-US interval in s (scalar or vector, in the latter case must be % defined for all trials, or cell array) % * method: one of '2026_short_uni', '2026_short_bi', '2026_long_uni', -% '2026_short_bi'. Here, 'long/short' refer to the isi in de Vries +% '2026_long_bi'. Here, 'long/short' refer to the isi in de Vries % et al. (which was 3.5 s for 'short', and > 6 s for 'long'), and % 'uni/bi' refers to unidirectional or bidirectional filtering % (where bidirectional filtering was slightly better in the tested @@ -39,7 +39,7 @@ sts = -1; out = []; -if nargin <= 5 +if nargin < 5 warning('Don''t know what to do'); return end @@ -55,18 +55,18 @@ %% Parse options and setup timings if ~iscell(fn) onsets = {onsets}; - isi = {isi}; + isi = {isi}; end for i_sn = 1:numel(onsets) - timing{i_sn}{1} = onsets{i_sn}(:) + isi{i_sn};(:) + timing{i_sn}{1} = onsets{i_sn}(:) + isi{i_sn}(:); if ismember(method, {'2026_short_uni', '2026_short_bi'}) % flex-fix timing{i_sn}{2} = [onsets{i_sn}(:); onsets{i_sn}(:) + isi{i_sn}(:)]; elseif ismember(method, {'2026_long_uni', '2026_long_bi'}) % flex-flex-fix with halved ISI - timing{i_sn}{2} = [onsets{i_sn}(:); onsets{i_sn}(:) + isi{i_sn}(:)/2]; - timing{i_sn}{3} = [onsets{i_sn}(:) + isi{i_sn}(:)/2; onsets{i_sn}(:) + isi{i_sn}(:)]; + timing{i_sn}{2} = [onsets{i_sn}(:), onsets{i_sn}(:) + isi{i_sn}(:)/2]; + timing{i_sn}{3} = [onsets{i_sn}(:) + isi{i_sn}(:)/2, onsets{i_sn}(:) + isi{i_sn}(:)]; else warning('Unknown method'); return @@ -79,15 +79,15 @@ %% Setup model model = struct( ... - datafile, fn, ... - missing, missing, ... - timing, timing, ... - norm, normalize); + 'datafile', fn, ... + 'missing', missing, ... + 'timing', [], ... % + 'norm', normalize); +model.timing = timing; % set (dummy) filename [pth, fn, ext] = fileparts(fn); model_fn = fullfile(pth, ['mdl_', fn, ext]); -model.modelfile = model_fn; - +model.modelfile = model_fn; if strcmpi(method, '2026_short_uni') elseif strcmpi(method, '2026_short_bi') elseif strcmpi(method, '2026_long_uni') @@ -100,6 +100,7 @@ % default model end + %% Setup options options = struct('overwrite', 0, 'nosave', 1-keepfile); From fcb5dc7495246a4d961e39cd9504376ab3a0ee0f Mon Sep 17 00:00:00 2001 From: Dominik Bach Date: Mon, 8 Dec 2025 09:04:52 +0100 Subject: [PATCH 3/8] add short ISI options --- src/pspm_pipeline_fc_scr.m | 44 +++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/pspm_pipeline_fc_scr.m b/src/pspm_pipeline_fc_scr.m index ed848721..864478f8 100644 --- a/src/pspm_pipeline_fc_scr.m +++ b/src/pspm_pipeline_fc_scr.m @@ -12,12 +12,13 @@ % * onsets: onset times of all CS (vector), or cell array of onset times % * isi: CS-US interval in s (scalar or vector, in the latter case must be % defined for all trials, or cell array) -% * method: one of '2026_short_uni', '2026_short_bi', '2026_long_uni', -% '2026_long_bi'. Here, 'long/short' refer to the isi in de Vries -% et al. (which was 3.5 s for 'short', and > 6 s for 'long'), and -% 'uni/bi' refers to unidirectional or bidirectional filtering -% (where bidirectional filtering was slightly better in the tested -% data sets but can be suboptimal with long inter-trial intervals) +% * method: one of '2026_short', '2026_long_uni', '2026_long_bi'. Here, +% 'long/short' refer to the isi in de Vries et al. (which was +% 3.5 s for 'short', and > 6 s for 'long'), and 'uni/bi' refers +% to unidirectional or bidirectional filtering (where bidirectional +% filtering was slightly better for long ISI in the tested +% data sets but can be suboptimal with long inter-trial intervals. +% For short ISI, unidirectional filtering was always better). % * normalize: [optional] Normalise data. Data are normalised during inversion % but results are transformed back into raw data units. Default: 0. % * keepfile: [optional] Save model file for diagnostic purposes. Default: 0. @@ -39,7 +40,7 @@ sts = -1; out = []; -if nargin < 5 +if nargin < 5 warning('Don''t know what to do'); return end @@ -55,18 +56,18 @@ %% Parse options and setup timings if ~iscell(fn) onsets = {onsets}; - isi = {isi}; + isi = {isi}; end for i_sn = 1:numel(onsets) timing{i_sn}{1} = onsets{i_sn}(:) + isi{i_sn}(:); - if ismember(method, {'2026_short_uni', '2026_short_bi'}) + if ismember(method, {'2026_short'}) % flex-fix timing{i_sn}{2} = [onsets{i_sn}(:); onsets{i_sn}(:) + isi{i_sn}(:)]; elseif ismember(method, {'2026_long_uni', '2026_long_bi'}) % flex-flex-fix with halved ISI - timing{i_sn}{2} = [onsets{i_sn}(:), onsets{i_sn}(:) + isi{i_sn}(:)/2]; - timing{i_sn}{3} = [onsets{i_sn}(:) + isi{i_sn}(:)/2, onsets{i_sn}(:) + isi{i_sn}(:)]; + timing{i_sn}{2} = [onsets{i_sn}(:); onsets{i_sn}(:) + isi{i_sn}(:)/2]; + timing{i_sn}{3} = [onsets{i_sn}(:) + isi{i_sn}(:)/2; onsets{i_sn}(:) + isi{i_sn}(:)]; else warning('Unknown method'); return @@ -78,18 +79,22 @@ end %% Setup model +% set (dummy) filename +[pth, fn, ext] = fileparts(fn); +model_fn = fullfile(pth, ['mdl_', fn, ext]); + model = struct( ... 'datafile', fn, ... 'missing', missing, ... - 'timing', [], ... % + 'modelfile', model_fn, ... 'norm', normalize); -model.timing = timing; -% set (dummy) filename -[pth, fn, ext] = fileparts(fn); -model_fn = fullfile(pth, ['mdl_', fn, ext]); -model.modelfile = model_fn; -if strcmpi(method, '2026_short_uni') -elseif strcmpi(method, '2026_short_bi') +model.timing = timing; + +if strcmpi(method, '2026_short') + model.constrained = 1; + % filter + model.filter = settings.dcm{1}.filter; + model.filter.direction = 'uni'; elseif strcmpi(method, '2026_long_uni') % this is a hidden option in pspm_dcm_inv (upper bound in s) model.constrained_upper = 1; @@ -100,7 +105,6 @@ % default model end - %% Setup options options = struct('overwrite', 0, 'nosave', 1-keepfile); From a80fb018020abc394517b73fd31efa7b6b2cdc7c Mon Sep 17 00:00:00 2001 From: 4gwe Date: Wed, 17 Dec 2025 13:29:41 +0100 Subject: [PATCH 4/8] small fixes --- src/pspm_pipeline_fc_scr.m | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pspm_pipeline_fc_scr.m b/src/pspm_pipeline_fc_scr.m index 864478f8..53f4cc24 100644 --- a/src/pspm_pipeline_fc_scr.m +++ b/src/pspm_pipeline_fc_scr.m @@ -66,8 +66,8 @@ timing{i_sn}{2} = [onsets{i_sn}(:); onsets{i_sn}(:) + isi{i_sn}(:)]; elseif ismember(method, {'2026_long_uni', '2026_long_bi'}) % flex-flex-fix with halved ISI - timing{i_sn}{2} = [onsets{i_sn}(:); onsets{i_sn}(:) + isi{i_sn}(:)/2]; - timing{i_sn}{3} = [onsets{i_sn}(:) + isi{i_sn}(:)/2; onsets{i_sn}(:) + isi{i_sn}(:)]; + timing{i_sn}{2} = [onsets{i_sn}(:), onsets{i_sn}(:) + isi{i_sn}(:)/2]; + timing{i_sn}{3} = [onsets{i_sn}(:) + isi{i_sn}(:)/2, onsets{i_sn}(:) + isi{i_sn}(:)]; else warning('Unknown method'); return @@ -80,8 +80,8 @@ %% Setup model % set (dummy) filename -[pth, fn, ext] = fileparts(fn); -model_fn = fullfile(pth, ['mdl_', fn, ext]); +[pth, fn_model, ext] = fileparts(fn); +model_fn = fullfile(pth, ['mdl_', fn_model, ext]); model = struct( ... 'datafile', fn, ... From af5c068c5e7a996b8dfbfd5820696a288c9bbfed Mon Sep 17 00:00:00 2001 From: 4gwe Date: Thu, 18 Dec 2025 08:50:46 +0100 Subject: [PATCH 5/8] multiple sessions works now --- src/pspm_pipeline_fc_scr.m | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/pspm_pipeline_fc_scr.m b/src/pspm_pipeline_fc_scr.m index 53f4cc24..652041b8 100644 --- a/src/pspm_pipeline_fc_scr.m +++ b/src/pspm_pipeline_fc_scr.m @@ -80,16 +80,23 @@ %% Setup model % set (dummy) filename -[pth, fn_model, ext] = fileparts(fn); -model_fn = fullfile(pth, ['mdl_', fn_model, ext]); +[pth, fn_m, ext] = fileparts(fn); +model_fn = fullfile(pth{1}, ['mdl_', fn_m{1}, '.mat']); model = struct( ... - 'datafile', fn, ... - 'missing', missing, ... 'modelfile', model_fn, ... 'norm', normalize); +model.datafile = fn; +model.missing = missing; model.timing = timing; +% sanity check +n = numel(model.datafile); +if ~(numel(model.missing) == n && numel(model.timing) == n) + waring('Not the same length!') + return +end + if strcmpi(method, '2026_short') model.constrained = 1; % filter From a63321cd1b21af4bfb4c3c15adf094f6995065ac Mon Sep 17 00:00:00 2001 From: 4gwe Date: Thu, 18 Dec 2025 09:07:08 +0100 Subject: [PATCH 6/8] small fix --- src/pspm_pipeline_fc_scr.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pspm_pipeline_fc_scr.m b/src/pspm_pipeline_fc_scr.m index 652041b8..5a1d5378 100644 --- a/src/pspm_pipeline_fc_scr.m +++ b/src/pspm_pipeline_fc_scr.m @@ -63,7 +63,7 @@ timing{i_sn}{1} = onsets{i_sn}(:) + isi{i_sn}(:); if ismember(method, {'2026_short'}) % flex-fix - timing{i_sn}{2} = [onsets{i_sn}(:); onsets{i_sn}(:) + isi{i_sn}(:)]; + timing{i_sn}{2} = [onsets{i_sn}(:), onsets{i_sn}(:) + isi{i_sn}(:)]; elseif ismember(method, {'2026_long_uni', '2026_long_bi'}) % flex-flex-fix with halved ISI timing{i_sn}{2} = [onsets{i_sn}(:), onsets{i_sn}(:) + isi{i_sn}(:)/2]; From 71a542ad5350e0d852283f1bee1f62bbcfc2ddf2 Mon Sep 17 00:00:00 2001 From: 4gwe Date: Thu, 18 Dec 2025 09:11:39 +0100 Subject: [PATCH 7/8] fix --- src/pspm_pipeline_fc_scr.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pspm_pipeline_fc_scr.m b/src/pspm_pipeline_fc_scr.m index 5a1d5378..b18539f1 100644 --- a/src/pspm_pipeline_fc_scr.m +++ b/src/pspm_pipeline_fc_scr.m @@ -93,7 +93,7 @@ % sanity check n = numel(model.datafile); if ~(numel(model.missing) == n && numel(model.timing) == n) - waring('Not the same length!') + warning('Not the same length!') return end From 24069ac71dde30d77a9f94c5305bb28cf5bb6870 Mon Sep 17 00:00:00 2001 From: 4gwe Date: Thu, 18 Dec 2025 09:13:01 +0100 Subject: [PATCH 8/8] sanity check removed --- src/pspm_pipeline_fc_scr.m | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/pspm_pipeline_fc_scr.m b/src/pspm_pipeline_fc_scr.m index b18539f1..e1fa4b6c 100644 --- a/src/pspm_pipeline_fc_scr.m +++ b/src/pspm_pipeline_fc_scr.m @@ -90,13 +90,6 @@ model.missing = missing; model.timing = timing; -% sanity check -n = numel(model.datafile); -if ~(numel(model.missing) == n && numel(model.timing) == n) - warning('Not the same length!') - return -end - if strcmpi(method, '2026_short') model.constrained = 1; % filter