From 1d5ba465e278c1de35e0f61ad2118fbcfe1f7ce1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:14:15 +0000 Subject: [PATCH 1/4] Initial plan From 4fa5a98f94d19b1593069c903be2b3e671ba78ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:18:31 +0000 Subject: [PATCH 2/4] feat: extend absoluteToRelativeEvents with name-value modes and trial inference Co-authored-by: Nabarb <23075957+Nabarb@users.noreply.github.com> --- @NeuralEmbedding/NeuralEmbedding.m | 141 ++++++++++++++++++++++++++--- 1 file changed, 130 insertions(+), 11 deletions(-) diff --git a/@NeuralEmbedding/NeuralEmbedding.m b/@NeuralEmbedding/NeuralEmbedding.m index fe90895..243ef52 100644 --- a/@NeuralEmbedding/NeuralEmbedding.m +++ b/@NeuralEmbedding/NeuralEmbedding.m @@ -259,7 +259,8 @@ function addEvents(obj,evts) % % To convert events with absolute timestamps to the required relative % format, use the static utility: - % evts = NeuralEmbedding.absoluteToRelativeEvents(evts, trialStartTimes) + % evts = NeuralEmbedding.absoluteToRelativeEvents(evts, ... + % 'trialStartReference', trialStartTimes) % % See also NeuralEmbedding.absoluteToRelativeEvents arguments @@ -1258,18 +1259,28 @@ function animate3(obj,maxT) end %% Usefull generic methods methods(Static) - function evts = absoluteToRelativeEvents(evts, trialStartTimes) + function evts = absoluteToRelativeEvents(evts, varargin) % ABSOLUTETORELATIVEEVENTS Convert event timestamps from absolute to relative time. - % EVTS = ABSOLUTETORELATIVEEVENTS(EVTS, TRIALSTARTTIMES) subtracts + % EVTS = ABSOLUTETORELATIVEEVENTS(EVTS, 'trialStartReference', REF) subtracts % each event's trial-start time from its Ts field, so that the % returned struct has Ts values relative to the beginning of the % trial (0 = trial alignment / trial start). % % Inputs: % evts - struct array with fields Ts (absolute recording time), - % name, trial, and optionally data. - % trialStartTimes - (1 x nTrials) or (nTrials x 1) vector of trial-start - % timestamps in the same absolute time base as evts.Ts. + % name, trial, and optionally data. Events without a + % valid Ts are discarded. + % trialStartReference - either: + % * numeric vector (1 x nTrials) or (nTrials x 1) with + % trial-start timestamps in the same absolute time base + % as evts.Ts, or + % * event name (char/string) present in evts. In this + % modality, for each trial the first event with that + % name is used as trial-start reference. + % inferTrialFromBounds - logical flag (default false). If true, and both + % 'trialstart' and 'trialend' events are present in evts, + % missing/invalid trial assignments are inferred by + % checking where each event Ts falls within those bounds. % % Output: % evts - same struct array with Ts converted to relative time. @@ -1281,19 +1292,127 @@ function animate3(obj,maxT) % evt.name = "reward"; % evt.trial = 2; % belongs to trial 2 (started at t=20) % evt.data = []; - % evt = NeuralEmbedding.absoluteToRelativeEvents(evt, trialStarts); + % evt = NeuralEmbedding.absoluteToRelativeEvents(evt, ... + % 'trialStartReference', trialStarts); % % evt.Ts is now 2 (= 22 - 20) % % See also NeuralEmbedding.addEvents arguments evts struct - trialStartTimes (1,:) double end evts = evts(:); % normalise to column vector ff = fieldnames(evts); - if ~ismember('Ts', ff) || ~ismember('trial', ff) + p = inputParser(); + p.FunctionName = 'NeuralEmbedding.absoluteToRelativeEvents'; + addParameter(p,'trialStartReference',[], ... + @(x) isnumeric(x) || ischar(x) || (isstring(x) && isscalar(x))); + addParameter(p,'inferTrialFromBounds',false, ... + @(x) islogical(x) && isscalar(x)); + parse(p,varargin{:}); + trialStartReference = p.Results.trialStartReference; + inferTrialFromBounds = p.Results.inferTrialFromBounds; + + if ~ismember('Ts', ff) + evts = evts([]); + return; + end + + hasTs = arrayfun(@(e) isnumeric(e.Ts) && isscalar(e.Ts) && ... + ~isempty(e.Ts) && isfinite(e.Ts), evts); + evts = evts(hasTs); + if isempty(evts) + return; + end + + ff = fieldnames(evts); + if ~ismember('trial', ff) + [evts.trial] = deal([]); + ff = fieldnames(evts); + end + + if inferTrialFromBounds + if ~ismember('name', ff) + error('NeuralEmbedding:absoluteToRelativeEvents:invalidFields', ... + 'Event structure must contain field name when inferTrialFromBounds is true.'); + end + evtNames = string({evts.name}); + isTrialStart = strcmpi(evtNames,'trialstart'); + isTrialEnd = strcmpi(evtNames,'trialend'); + if any(isTrialStart) && any(isTrialEnd) + tStart = [evts(isTrialStart).Ts]; + tEnd = [evts(isTrialEnd).Ts]; + nBounds = min(numel(tStart), numel(tEnd)); + tStart = tStart(1:nBounds); + tEnd = tEnd(1:nBounds); + validBounds = tEnd >= tStart; + tStart = tStart(validBounds); + tEnd = tEnd(validBounds); + + if ~isempty(tStart) + for i = 1:numel(evts) + trial = evts(i).trial; + hasValidTrial = isnumeric(trial) && isscalar(trial) && ... + ~isempty(trial) && isfinite(trial) && ... + trial >= 1 && mod(trial,1) == 0; + if ~hasValidTrial + match = find(evts(i).Ts >= tStart & evts(i).Ts <= tEnd, 1, 'first'); + if ~isempty(match) + evts(i).trial = match; + end + end + end + end + end + end + + if isempty(trialStartReference) + error('NeuralEmbedding:absoluteToRelativeEvents:missingTrialStartReference', ... + ['Missing trial-start reference. Provide ', ... + '''trialStartReference'' as numeric trial-start times ', ... + 'or as an event name present in evts.']); + end + + if isnumeric(trialStartReference) + trialStartTimes = trialStartReference(:)'; + else + if ~ismember('name', ff) + error('NeuralEmbedding:absoluteToRelativeEvents:invalidFields', ... + 'Event structure must contain fields Ts, name, trial.'); + end + + refName = string(trialStartReference); + if strlength(refName) == 0 + error('NeuralEmbedding:absoluteToRelativeEvents:invalidTrialStartReference', ... + 'trialStartReference event name must be non-empty.'); + end + + validTrialMask = arrayfun(@(e) isnumeric(e.trial) && isscalar(e.trial) && ... + ~isempty(e.trial) && isfinite(e.trial) && ... + e.trial >= 1 && mod(e.trial,1) == 0, evts); + if ~all(validTrialMask) + error('NeuralEmbedding:absoluteToRelativeEvents:invalidTrial', ... + ['All events must have a valid trial index to use ', ... + 'an event name as trialStartReference.']); + end + + trialIdx = [evts.trial]; + nTrials = max(trialIdx); + evtNames = string({evts.name}); + isRefEvent = strcmp(evtNames, refName); + trialStartTimes = nan(1,nTrials); + for trial = 1:nTrials + idx = find(isRefEvent & trialIdx == trial, 1, 'first'); + if isempty(idx) + error('NeuralEmbedding:absoluteToRelativeEvents:missingTrialStartEvent', ... + 'No event named %s found for trial %d.', refName, trial); + end + trialStartTimes(trial) = evts(idx).Ts; + end + end + + if ~ismember('trial', ff) error('NeuralEmbedding:absoluteToRelativeEvents:invalidFields', ... 'Event structure must contain at least the fields: Ts, trial.'); end @@ -1301,7 +1420,8 @@ function animate3(obj,maxT) nTrials = numel(trialStartTimes); for i = 1:numel(evts) trial = evts(i).trial; - if trial < 1 || trial > nTrials + if ~isnumeric(trial) || ~isscalar(trial) || isempty(trial) || ... + ~isfinite(trial) || trial < 1 || mod(trial,1) ~= 0 || trial > nTrials error('NeuralEmbedding:absoluteToRelativeEvents:invalidTrial', ... 'Trial index %d is out of range [1, %d].', trial, nTrials); end @@ -1599,4 +1719,3 @@ function animate3(obj,maxT) end end - From 104bd56900406c9e04b99bc051a1d365ebbf0e43 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:19:09 +0000 Subject: [PATCH 3/4] chore: address code review feedback for absoluteToRelativeEvents Co-authored-by: Nabarb <23075957+Nabarb@users.noreply.github.com> --- @NeuralEmbedding/NeuralEmbedding.m | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/@NeuralEmbedding/NeuralEmbedding.m b/@NeuralEmbedding/NeuralEmbedding.m index 243ef52..2030658 100644 --- a/@NeuralEmbedding/NeuralEmbedding.m +++ b/@NeuralEmbedding/NeuralEmbedding.m @@ -1406,24 +1406,19 @@ function animate3(obj,maxT) idx = find(isRefEvent & trialIdx == trial, 1, 'first'); if isempty(idx) error('NeuralEmbedding:absoluteToRelativeEvents:missingTrialStartEvent', ... - 'No event named %s found for trial %d.', refName, trial); + 'No event named %s found for trial %d.', char(refName), trial); end trialStartTimes(trial) = evts(idx).Ts; end end - if ~ismember('trial', ff) - error('NeuralEmbedding:absoluteToRelativeEvents:invalidFields', ... - 'Event structure must contain at least the fields: Ts, trial.'); - end - nTrials = numel(trialStartTimes); for i = 1:numel(evts) trial = evts(i).trial; if ~isnumeric(trial) || ~isscalar(trial) || isempty(trial) || ... ~isfinite(trial) || trial < 1 || mod(trial,1) ~= 0 || trial > nTrials error('NeuralEmbedding:absoluteToRelativeEvents:invalidTrial', ... - 'Trial index %d is out of range [1, %d].', trial, nTrials); + 'Event trial index is invalid or out of range [1, %d].', nTrials); end evts(i).Ts = evts(i).Ts - trialStartTimes(trial); end @@ -1718,4 +1713,3 @@ function animate3(obj,maxT) end end end - From 9bdf614639b5e3e2e754ceb3dec008ed6e4067d8 Mon Sep 17 00:00:00 2001 From: fbarban Date: Tue, 17 Mar 2026 18:05:11 +0100 Subject: [PATCH 4/4] feat: make absoluteToRelativeEvents accept name-value args and robustly handle trial bounds Replace inputParser with a name-value arguments interface for absoluteToRelativeEvents, add an input validator for trialStartReference, and expose inferTrialFromBounds as a named option. Improve robustness when pairing trial start/end timestamps by trimming unmatched leading ends or trailing starts and only keeping properly ordered start/end pairs. Minor tidy-ups to variable formatting. --- @NeuralEmbedding/NeuralEmbedding.m | 49 +++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/@NeuralEmbedding/NeuralEmbedding.m b/@NeuralEmbedding/NeuralEmbedding.m index 2030658..d5b5c61 100644 --- a/@NeuralEmbedding/NeuralEmbedding.m +++ b/@NeuralEmbedding/NeuralEmbedding.m @@ -1259,7 +1259,7 @@ function animate3(obj,maxT) end %% Usefull generic methods methods(Static) - function evts = absoluteToRelativeEvents(evts, varargin) + function evts = absoluteToRelativeEvents(evts, namevalue) % ABSOLUTETORELATIVEEVENTS Convert event timestamps from absolute to relative time. % EVTS = ABSOLUTETORELATIVEEVENTS(EVTS, 'trialStartReference', REF) subtracts % each event's trial-start time from its Ts field, so that the @@ -1298,21 +1298,17 @@ function animate3(obj,maxT) % % See also NeuralEmbedding.addEvents arguments - evts struct + evts struct + namevalue.inferTrialFromBounds {mustBeNumArrayOrString} + namevalue.trialStartReference (1,1) logical = false end evts = evts(:); % normalise to column vector ff = fieldnames(evts); - p = inputParser(); - p.FunctionName = 'NeuralEmbedding.absoluteToRelativeEvents'; - addParameter(p,'trialStartReference',[], ... - @(x) isnumeric(x) || ischar(x) || (isstring(x) && isscalar(x))); - addParameter(p,'inferTrialFromBounds',false, ... - @(x) islogical(x) && isscalar(x)); - parse(p,varargin{:}); - trialStartReference = p.Results.trialStartReference; - inferTrialFromBounds = p.Results.inferTrialFromBounds; + + trialStartReference = namevalue.trialStartReference; + inferTrialFromBounds = namevalue.inferTrialFromBounds; if ~ismember('Ts', ff) evts = evts([]); @@ -1342,13 +1338,28 @@ function animate3(obj,maxT) isTrialEnd = strcmpi(evtNames,'trialend'); if any(isTrialStart) && any(isTrialEnd) tStart = [evts(isTrialStart).Ts]; - tEnd = [evts(isTrialEnd).Ts]; + tEnd = [evts(isTrialEnd).Ts]; + + % Handle simple boundary mismatches: + % 1) unmatched end at the beginning + while ~isempty(tStart) && ~isempty(tEnd) && tEnd(1) < tStart(1) + tEnd(1) = []; + end + + % 2) unmatched start at the end + while ~isempty(tStart) && ~isempty(tEnd) && tStart(end) > tEnd(end) + tStart(end) = []; + end + + % Pair remaining starts/ends nBounds = min(numel(tStart), numel(tEnd)); tStart = tStart(1:nBounds); - tEnd = tEnd(1:nBounds); + tEnd = tEnd(1:nBounds); + + % Keep only properly ordered pairs validBounds = tEnd >= tStart; tStart = tStart(validBounds); - tEnd = tEnd(validBounds); + tEnd = tEnd(validBounds); if ~isempty(tStart) for i = 1:numel(evts) @@ -1422,6 +1433,16 @@ function animate3(obj,maxT) end evts(i).Ts = evts(i).Ts - trialStartTimes(trial); end + + + end + + function mustBeNumArrayOrString(x) + if not(isnumeric(x) || ischar(x) || (isstring(x) && isscalar(x))) + eidType = 'mustBeNumArrayOrString:notNumArrayOrString'; + msgType = 'Input must be a numeric array or a scalar string (or char vector).'; + error(eidType,msgType); + end end % Gaussian kernel smoothing of data across time