Skip to content

First trial missing extractor #919

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions brainbox/examples/.idea/.gitignore

This file was deleted.

11 changes: 0 additions & 11 deletions brainbox/examples/.idea/examples.iml

This file was deleted.

This file was deleted.

7 changes: 0 additions & 7 deletions brainbox/examples/.idea/misc.xml

This file was deleted.

8 changes: 0 additions & 8 deletions brainbox/examples/.idea/modules.xml

This file was deleted.

6 changes: 0 additions & 6 deletions brainbox/examples/.idea/vcs.xml

This file was deleted.

55 changes: 45 additions & 10 deletions ibllib/io/extractors/ephys_fpga.py
Original file line number Diff line number Diff line change
Expand Up @@ -806,8 +806,13 @@ def _extract(self, sync=None, chmap=None, sync_collection='raw_ephys_data',
fcn, *_ = ibldsp.utils.sync_timestamps(bpod_start, t_trial_start)
buffer = 2.5 # the number of seconds to include before/after task
start, end = fcn(self.bpod_trials['intervals'].flat[[0, -1]])
tmin = min(sync['times'][0], start - buffer)
tmax = max(sync['times'][-1], end + buffer)
# NB: The following was added by k1o0 in commit b31d14e5113180b50621c985b2f230ba84da1dd3
# however it is not clear why this was necessary and it appears to defeat the purpose of
# removing the passive protocol part from the final trial extraction in ephysChoiceWorld.
# tmin = min(sync['times'][0], start - buffer)
# tmax = max(sync['times'][-1], end + buffer)
tmin = start - buffer
tmax = end + buffer
else: # This type of alignment fails for some sessions, e.g. mesoscope
tmin = tmax = None

Expand Down Expand Up @@ -934,22 +939,50 @@ def build_trials(self, sync, chmap, display=False, **kwargs):
# Sync the Bpod clock to the DAQ.
# NB: The Bpod extractor typically drops the final, incomplete, trial. Hence there is
# usually at least one extra FPGA event. This shouldn't affect the sync. The final trial is
# dropped after assigning the FPGA events, using the `ifpga` index. Doing this after
# dropped after assigning the FPGA events, using the `ibpod` index. Doing this after
# assigning the FPGA trial events ensures the last trial has the correct timestamps.
self.bpod2fpga, drift_ppm, ibpod, ifpga = self.sync_bpod_clock(self.bpod_trials, fpga_events, self.sync_field)

if np.any(np.diff(ibpod) != 1) and self.sync_field == 'intervals_0':
bpod_start = self.bpod2fpga(self.bpod_trials['intervals'][:, 0])
missing_bpod_idx = np.setxor1d(ibpod, np.arange(len(bpod_start)))
if missing_bpod_idx.size > 0 and self.sync_field == 'intervals_0':
# One issue is that sometimes pulses may not have been detected, in this case
# add the events that have not been detected and re-extract the behaviour sync.
# This is only really relevant for the Bpod interval events as the other TTLs are
# from devices where a missing TTL likely means the Bpod event was truly absent.
_logger.warning('Missing Bpod TTLs; reassigning events using aligned Bpod start times')
bpod_start = self.bpod_trials['intervals'][:, 0]
missing_bpod = self.bpod2fpga(bpod_start[np.setxor1d(ibpod, np.arange(len(bpod_start)))])
t_trial_start = np.sort(np.r_[fpga_events['intervals_0'][:, 0], missing_bpod])
missing_bpod = bpod_start[missing_bpod_idx]
# Another complication: if the first trial start is missing on the FPGA, the second
# trial start is assumed to be the first and is mis-assigned to another trial event
# (i.e. valve open). This is done because the first Bpod pulse is irregularly long.
# See `FpgaTrials.get_bpod_event_times` for details.

# If first trial start is missing first detected FPGA event doesn't match any Bpod
# starts then it's probably a mis-assigned valve or trial end event.
i1 = np.any(missing_bpod_idx == 0) and not np.any(np.isclose(fpga_events['intervals_0'][0], bpod_start))
# skip mis-assigned first FPGA trial start
t_trial_start = np.sort(np.r_[fpga_events['intervals_0'][int(i1):], missing_bpod])
ibpod = np.sort(np.r_[ibpod, missing_bpod_idx])
if i1:
# The first trial start is actually the first valve open here
first_on, first_off = bpod_event_intervals['trial_start'][0, :]
bpod_valve_open = self.bpod2fpga(self.bpod_trials['feedback_times'][self.bpod_trials['feedbackType'] == 1])
if np.any(np.isclose(first_on, bpod_valve_open)):
# Probably assigned to the valve open
_logger.debug('Re-reassigning first valve open event. TTL length = %.3g ms', first_off - first_on)
fpga_events['valveOpen_times'] = np.sort(np.r_[first_on, fpga_events['valveOpen_times']])
fpga_events['valveClose_times'] = np.sort(np.r_[first_off, fpga_events['valveClose_times']])
elif np.any(np.isclose(first_on, self.bpod2fpga(self.bpod_trials['itiIn_times']))):
# Probably assigned to the trial end
_logger.debug('Re-reassigning first trial end event. TTL length = %.3g ms', first_off - first_on)
fpga_events['itiIn_times'] = np.sort(np.r_[first_on, fpga_events['itiIn_times']])
fpga_events['intervals_1'] = np.sort(np.r_[first_off, fpga_events['intervals_1']])
else:
_logger.warning('Unable to reassign first trial start event. TTL length = %.3g ms', first_off - first_on)
# Bpod trial_start event intervals are not used but for consistency we'll update them here anyway
bpod_event_intervals['trial_start'] = bpod_event_intervals['trial_start'][1:, :]
else:
t_trial_start = fpga_events['intervals_0']
t_trial_start = t_trial_start[ifpga]

out = alfio.AlfBunch()
# Add the Bpod trial events, converting the timestamp fields to FPGA time.
Expand Down Expand Up @@ -1000,10 +1033,12 @@ def build_trials(self, sync, chmap, display=False, **kwargs):
ind_err = np.isnan(fpga_trials['valveOpen_times'])
fpga_trials['feedback_times'][ind_err] = fpga_trials['errorCue_times'][ind_err]

out.update({k: fpga_trials[k] for k in fpga_trials.keys()})
# Use ibpod to discard the final trial if it is incomplete
# ibpod should be indices of all Bpod trials, even those that were not detected on the FPGA
out.update({k: fpga_trials[k][ibpod] for k in fpga_trials.keys()})

if display: # pragma: no cover
width = 0.5
width = 2
ymax = 5
if isinstance(display, bool):
plt.figure('Bpod FPGA Sync')
Expand Down
Loading