Skip to content

Commit a2dec1f

Browse files
committed
Merge branch 'hotfix/2.0.7'
2 parents eee691b + a0282c8 commit a2dec1f

File tree

14 files changed

+281
-61
lines changed

14 files changed

+281
-61
lines changed

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[flake8]
2-
max-line-length = 99
2+
max-line-length = 130
33
ignore = W504, W503, E266
44
exclude =
55
.git,

brainbox/plot.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@ def peri_event_time_histogram(
618618

619619

620620
def driftmap(ts, feat, ax=None, plot_style='bincount',
621-
t_bin=0.01, d_bin=20, weights=None, **kwargs):
621+
t_bin=0.01, d_bin=20, weights=None, vmax=None, **kwargs):
622622
"""
623623
Plots the values of a spike feature array (y-axis) over time (x-axis).
624624
Two arguments can be given for the plot_style of the drift map:
@@ -636,6 +636,7 @@ def driftmap(ts, feat, ax=None, plot_style='bincount',
636636
t_bin: time bin used when plot_style='bincount'
637637
d_bin: depth bin used when plot_style='bincount'
638638
plot_style: 'scatter', 'bincount'
639+
**kwargs: matplotlib.imshow arguments
639640
640641
Returns
641642
-------
@@ -674,7 +675,7 @@ def driftmap(ts, feat, ax=None, plot_style='bincount',
674675
R, times, depths = bincount2D(
675676
ts[iok], feat[iok], t_bin, d_bin, weights=weights)
676677
# plot raster map
677-
ax.imshow(R, aspect='auto', cmap='binary', vmin=0, vmax=np.std(R) * 4,
678+
ax.imshow(R, aspect='auto', cmap='binary', vmin=0, vmax=vmax or np.std(R) * 4,
678679
extent=np.r_[times[[0, -1]], depths[[0, -1]]], origin='lower', **kwargs)
679680
ax.set_xlabel('time (secs)')
680681
ax.set_ylabel('depth (um)')

brainbox/task/passive.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,17 @@ def get_rf_map_over_depth(rf_map_times, rf_map_pos, rf_stim_frames, spike_times,
115115
stim_on_times = rf_map_times[stim_frame[0]]
116116
stim_intervals = np.c_[stim_on_times - pre_stim, stim_on_times + post_stim]
117117

118-
idx_intervals = np.searchsorted(times, stim_intervals)
119-
120-
stim_trials = np.zeros((depths.shape[0], n_bins, idx_intervals.shape[0]))
121-
for i, on in enumerate(idx_intervals):
122-
stim_trials[:, :, i] = binned_array[:, on[0]:on[1]]
123-
avg_stim_trials = np.mean(stim_trials, axis=2)
118+
out_intervals = stim_intervals[:, 1] > times[-1]
119+
idx_intervals = np.searchsorted(times, stim_intervals)[np.invert(out_intervals)]
120+
121+
# Case when no spikes during the passive period
122+
if idx_intervals.shape[0] == 0:
123+
avg_stim_trials = np.zeros((depths.shape[0], n_bins))
124+
else:
125+
stim_trials = np.zeros((depths.shape[0], n_bins, idx_intervals.shape[0]))
126+
for i, on in enumerate(idx_intervals):
127+
stim_trials[:, :, i] = binned_array[:, on[0]:on[1]]
128+
avg_stim_trials = np.mean(stim_trials, axis=2)
124129

125130
_rf_map[:, x_pos, y_pos, :] = avg_stim_trials
126131

@@ -197,8 +202,10 @@ def get_stim_aligned_activity(stim_events, spike_times, spike_depths, z_score_fl
197202

198203
stim_intervals = np.c_[stim_times - pre_stim, stim_times + post_stim]
199204
base_intervals = np.c_[stim_times - base_stim, stim_times - pre_stim]
200-
idx_stim = np.searchsorted(times, stim_intervals)
201-
idx_base = np.searchsorted(times, base_intervals)
205+
out_intervals = stim_intervals[:, 1] > times[-1]
206+
207+
idx_stim = np.searchsorted(times, stim_intervals)[np.invert(out_intervals)]
208+
idx_base = np.searchsorted(times, base_intervals)[np.invert(out_intervals)]
202209

203210
stim_trials = np.zeros((depths.shape[0], n_bins, idx_stim.shape[0]))
204211
noise_trials = np.zeros((depths.shape[0], n_bins_base, idx_stim.shape[0]))

ibllib/io/extractors/camera.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ def _extract(self, sync=None, chmap=None, video_path=None,
9191
"""
9292
fpga_times = extract_camera_sync(sync=sync, chmap=chmap)
9393
count, (*_, gpio) = raw.load_embedded_frame_data(self.session_path, self.label)
94+
raw_ts = fpga_times[self.label]
95+
96+
if video_path is None:
97+
filename = f'_iblrig_{self.label}Camera.raw.mp4'
98+
video_path = self.session_path.joinpath('raw_video_data', filename)
99+
# Permit the video path to be the length for development and debugging purposes
100+
length = (video_path if isinstance(video_path, int) else get_video_length(video_path))
101+
_logger.debug(f'Number of video frames = {length}')
94102

95103
if gpio is not None and gpio['indices'].size > 1:
96104
_logger.info('Aligning to audio TTLs')
@@ -111,19 +119,10 @@ def _extract(self, sync=None, chmap=None, video_path=None,
111119
right at the end of the video. We therefore simply shorten the arrays to match
112120
the length of the video.
113121
"""
114-
if video_path is None:
115-
filename = f'_iblrig_{self.label}Camera.raw.mp4'
116-
video_path = self.session_path.joinpath('raw_video_data', filename)
117-
# Permit the video path to be the length for development and debugging purposes
118-
length = (video_path
119-
if isinstance(video_path, int)
120-
else get_video_length(video_path))
121-
_logger.debug(f'Number of video frames = {length}')
122122
if count.size > length:
123123
count = count[:length]
124124
else:
125125
assert length == count.size, 'fewer counts than frames'
126-
raw_ts = fpga_times[self.label]
127126
return align_with_audio(raw_ts, audio, gpio, count,
128127
display=display,
129128
extrapolate_missing=extrapolate_missing)
@@ -132,7 +131,11 @@ def _extract(self, sync=None, chmap=None, video_path=None,
132131

133132
# If you reach here extracting using audio TTLs was not possible
134133
_logger.warning('Alignment by wheel data not yet implemented')
135-
return fpga_times[self.label]
134+
if length < raw_ts.size:
135+
df = raw_ts.size - length
136+
_logger.info(f'Discarding first {df} pulses')
137+
raw_ts = raw_ts[df:]
138+
return raw_ts
136139

137140

138141
class CameraTimestampsBpod(BaseBpodTrialsExtractor):
@@ -219,6 +222,8 @@ def _extract(self, video_path=None, display=False, extrapolate_missing=True):
219222
raw_ts = np.r_[raw_ts, to_app] # Append the missing times
220223
elif n_missing < 0:
221224
_logger.warning(f'{abs(n_missing)} fewer frames than Bpod timestamps')
225+
_logger.info(f'Discarding first {abs(n_missing)} pulses')
226+
raw_ts = raw_ts[abs(n_missing):]
222227

223228
return raw_ts
224229

ibllib/io/extractors/ephys_fpga.py

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -245,28 +245,34 @@ def _rotary_encoder_positions_from_fronts(ta, pa, tb, pb, ticks=WHEEL_TICKS, rad
245245
return t, p
246246

247247

248-
def _assign_events_audio(audio_t, audio_polarities, return_indices=False):
248+
def _assign_events_audio(audio_t, audio_polarities, return_indices=False, display=False):
249249
"""
250250
From detected fronts on the audio sync traces, outputs the synchronisation events
251251
related to tone in
252252
253253
:param audio_t: numpy vector containing times of fronts
254254
:param audio_fronts: numpy vector containing polarity of fronts (1 rise, -1 fall)
255255
:param return_indices (False): returns indices of tones
256+
:param display (False): for debug mode, displays the raw fronts overlaid with detections
256257
:return: numpy arrays t_ready_tone_in, t_error_tone_in
257258
:return: numpy arrays ind_ready_tone_in, ind_error_tone_in if return_indices=True
258259
"""
259260
# make sure that there are no 2 consecutive fall or consecutive rise events
260261
assert(np.all(np.abs(np.diff(audio_polarities)) == 2))
261262
# take only even time differences: ie. from rising to falling fronts
262-
i0 = 0 if audio_polarities[0] == 1 else 1
263-
dt = np.diff(audio_t)[i0::2]
263+
dt = np.diff(audio_t)
264264
# detect ready tone by length below 110 ms
265-
i_ready_tone_in = np.r_[np.where(dt <= 0.11)[0] * 2]
265+
i_ready_tone_in = np.where(np.logical_and(dt <= 0.11, audio_polarities[:-1] == 1))[0]
266266
t_ready_tone_in = audio_t[i_ready_tone_in]
267-
# error tones are events lasting from 400ms to 600ms
268-
i_error_tone_in = np.where(np.logical_and(0.4 < dt, dt < 1.2))[0] * 2
267+
# error tones are events lasting from 400ms to 1200ms
268+
i_error_tone_in = np.where(np.logical_and(np.logical_and(0.4 < dt, dt < 1.2), audio_polarities[:-1] == 1))[0]
269269
t_error_tone_in = audio_t[i_error_tone_in]
270+
if display: # pragma: no cover
271+
from ibllib.plots import squares, vertical_lines
272+
squares(audio_t, audio_polarities, yrange=[-1, 1],)
273+
vertical_lines(t_ready_tone_in, ymin=-.8, ymax=.8)
274+
vertical_lines(t_error_tone_in, ymin=-.8, ymax=.8)
275+
270276
if return_indices:
271277
return t_ready_tone_in, t_error_tone_in, i_ready_tone_in, i_error_tone_in
272278
else:
@@ -280,7 +286,6 @@ def _assign_events_to_trial(t_trial_start, t_event, take='last'):
280286
Trials without an event
281287
result in nan value in output time vector.
282288
The output has a consistent size with t_trial_start and ready to output to alf.
283-
284289
:param t_trial_start: numpy vector of trial start times
285290
:param t_event: numpy vector of event times to assign to trials
286291
:param take: 'last' or 'first' (optional, default 'last'): index to take in case of duplicates
@@ -325,6 +330,37 @@ def get_sync_fronts(sync, channel_nb, tmin=None, tmax=None):
325330
'polarities': sync['polarities'][selection]})
326331

327332

333+
def _clean_audio(audio, display=False):
334+
"""
335+
one guy wired the 150 Hz camera output onto the soundcard. The effect is to get 150 Hz periodic
336+
square pulses, 2ms up and 4.666 ms down. When this happens we remove all of the intermediate
337+
pulses to repair the audio trace
338+
Here is some helper code
339+
dd = np.diff(audio['times'])
340+
1 / np.median(dd[::2]) # 2ms up
341+
1 / np.median(dd[1::2]) # 4.666 ms down
342+
1 / (np.median(dd[::2]) + np.median(dd[1::2])) # both sum to 150 Hx
343+
This only runs on sessions when the bug is detected and leaves others untouched
344+
"""
345+
DISCARD_THRESHOLD = 0.01
346+
average_150_hz = np.mean(1 / np.diff(audio['times'][audio['polarities'] == 1]) > 140)
347+
naudio = audio['times'].size
348+
if average_150_hz > 0.7 and naudio > 100:
349+
_logger.warning("Soundcard signal on FPGA seems to have been mixed with 150Hz camera")
350+
keep_ind = np.r_[np.diff(audio['times']) > DISCARD_THRESHOLD, False]
351+
keep_ind = np.logical_and(keep_ind, audio['polarities'] == -1)
352+
keep_ind = np.where(keep_ind)[0]
353+
keep_ind = np.sort(np.r_[0, keep_ind, keep_ind + 1, naudio - 1])
354+
355+
if display: # pragma: no cover
356+
from ibllib.plots import squares
357+
squares(audio['times'], audio['polarities'], ax=None, yrange=[-1, 1])
358+
squares(audio['times'][keep_ind], audio['polarities'][keep_ind], yrange=[-1, 1])
359+
audio = {'times': audio['times'][keep_ind],
360+
'polarities': audio['polarities'][keep_ind]}
361+
return audio
362+
363+
328364
def _clean_frame2ttl(frame2ttl, display=False):
329365
"""
330366
Frame 2ttl calibration can be unstable and the fronts may be flickering at an unrealistic
@@ -387,6 +423,7 @@ def extract_behaviour_sync(sync, chmap=None, display=False, bpod_trials=None):
387423
frame2ttl = get_sync_fronts(sync, chmap['frame2ttl'])
388424
frame2ttl = _clean_frame2ttl(frame2ttl)
389425
audio = get_sync_fronts(sync, chmap['audio'])
426+
audio = _clean_audio(audio)
390427
# extract events from the fronts for each trace
391428
t_trial_start, t_valve_open, t_iti_in = _assign_events_bpod(bpod['times'], bpod['polarities'])
392429
# one issue is that sometimes bpod pulses may not have been detected, in this case

ibllib/io/extractors/ephys_passive.py

Lines changed: 77 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,20 @@ def _load_passive_session_fixtures(session_path: str) -> dict:
7979
return fixture
8080

8181

82+
def _load_task_protocol(session_path: str) -> str:
83+
"""Find the IBL rig version used for the session
84+
85+
:param session_path: the path to a session
86+
:type session_path: str
87+
:return: ibl rig task protocol version
88+
:rtype: str
89+
"""
90+
settings = rawio.load_settings(session_path)
91+
ses_ver = settings["IBLRIG_VERSION_TAG"]
92+
93+
return ses_ver
94+
95+
8296
def _load_passive_stim_meta() -> dict:
8397
"""load_passive_stim_meta Loads the passive protocol metadata
8498
@@ -321,38 +335,72 @@ def _extract_passiveValve_intervals(bpod: dict) -> np.array:
321335

322336
# check all values are within bpod tolerance of 100µs
323337
assert np.allclose(
324-
valveOff_times - valveOn_times, valveOff_times[0] - valveOn_times[0], atol=0.0001
338+
valveOff_times - valveOn_times, valveOff_times[1] - valveOn_times[1], atol=0.0001
325339
), "Some valve outputs are longer or shorter than others"
326340

327341
return np.array([(x, y) for x, y in zip(valveOn_times, valveOff_times)])
328342

329343

330-
def _extract_passiveAudio_intervals(audio: dict) -> Tuple[np.array, np.array]:
331-
# Get Tone and Noise cue intervals
344+
def _extract_passiveAudio_intervals(audio: dict, rig_version: str) -> Tuple[np.array, np.array]:
345+
346+
# make an exception for task version = 6.2.5 where things are strange but data is recoverable
347+
if rig_version == '6.2.5':
348+
# Get all sound onsets and offsets
349+
soundOn_times = audio["times"][audio["polarities"] > 0]
350+
soundOff_times = audio["times"][audio["polarities"] < 0]
332351

333-
# Get all sound onsets and offsets
334-
soundOn_times = audio["times"][audio["polarities"] > 0]
335-
soundOff_times = audio["times"][audio["polarities"] < 0]
336-
# Check they are the correct number
337-
assert len(soundOn_times) == NTONES + NNOISES, "Wrong number of sound ONSETS"
338-
assert len(soundOff_times) == NTONES + NNOISES, "Wrong number of sound OFFSETS"
352+
# Have a couple that are wayyy too long!
353+
time_threshold = 10
354+
diff = soundOff_times - soundOn_times
355+
stupid = np.where(diff > time_threshold)[0]
356+
NREMOVE = len(stupid)
357+
not_stupid = np.where(diff < time_threshold)[0]
339358

340-
diff = soundOff_times - soundOn_times
341-
# Tone is ~100ms so check if diff < 0.3
342-
toneOn_times = soundOn_times[diff <= 0.3]
343-
toneOff_times = soundOff_times[diff <= 0.3]
344-
# Noise is ~500ms so check if diff > 0.3
345-
noiseOn_times = soundOn_times[diff > 0.3]
346-
noiseOff_times = soundOff_times[diff > 0.3]
359+
assert len(soundOn_times) == NTONES + NNOISES - NREMOVE, "Wrong number of sound ONSETS"
360+
assert len(soundOff_times) == NTONES + NNOISES - NREMOVE, "Wrong number of sound OFFSETS"
347361

348-
assert len(toneOn_times) == NTONES
349-
assert len(toneOff_times) == NTONES
350-
assert len(noiseOn_times) == NNOISES
351-
assert len(noiseOff_times) == NNOISES
362+
soundOn_times = soundOn_times[not_stupid]
363+
soundOff_times = soundOff_times[not_stupid]
352364

353-
# Fixed delays from soundcard ~500µs
354-
np.allclose(toneOff_times - toneOn_times, 0.1, atol=0.0006)
355-
np.allclose(noiseOff_times - noiseOn_times, 0.5, atol=0.0006)
365+
diff = soundOff_times - soundOn_times
366+
# Tone is ~100ms so check if diff < 0.3
367+
toneOn_times = soundOn_times[diff <= 0.3]
368+
toneOff_times = soundOff_times[diff <= 0.3]
369+
# Noise is ~500ms so check if diff > 0.3
370+
noiseOn_times = soundOn_times[diff > 0.3]
371+
noiseOff_times = soundOff_times[diff > 0.3]
372+
373+
# append with nans
374+
toneOn_times = np.r_[toneOn_times, np.full((NTONES - len(toneOn_times)), np.NAN)]
375+
toneOff_times = np.r_[toneOff_times, np.full((NTONES - len(toneOff_times)), np.NAN)]
376+
noiseOn_times = np.r_[noiseOn_times, np.full((NNOISES - len(noiseOn_times)), np.NAN)]
377+
noiseOff_times = np.r_[noiseOff_times, np.full((NNOISES - len(noiseOff_times)), np.NAN)]
378+
379+
else:
380+
# Get all sound onsets and offsets
381+
soundOn_times = audio["times"][audio["polarities"] > 0]
382+
soundOff_times = audio["times"][audio["polarities"] < 0]
383+
384+
# Check they are the correct number
385+
assert len(soundOn_times) == NTONES + NNOISES, "Wrong number of sound ONSETS"
386+
assert len(soundOff_times) == NTONES + NNOISES, "Wrong number of sound OFFSETS"
387+
388+
diff = soundOff_times - soundOn_times
389+
# Tone is ~100ms so check if diff < 0.3
390+
toneOn_times = soundOn_times[diff <= 0.3]
391+
toneOff_times = soundOff_times[diff <= 0.3]
392+
# Noise is ~500ms so check if diff > 0.3
393+
noiseOn_times = soundOn_times[diff > 0.3]
394+
noiseOff_times = soundOff_times[diff > 0.3]
395+
396+
assert len(toneOn_times) == NTONES
397+
assert len(toneOff_times) == NTONES
398+
assert len(noiseOn_times) == NNOISES
399+
assert len(noiseOff_times) == NNOISES
400+
401+
# Fixed delays from soundcard ~500µs
402+
np.allclose(toneOff_times - toneOn_times, 0.1, atol=0.0006)
403+
np.allclose(noiseOff_times - noiseOn_times, 0.5, atol=0.0006)
356404

357405
passiveTone_intervals = np.append(
358406
toneOn_times.reshape((len(toneOn_times), 1)),
@@ -444,8 +492,10 @@ def extract_task_replay(
444492
bpod = ephys_fpga.get_sync_fronts(sync, sync_map["bpod"], tmin=treplay[0])
445493
passiveValve_intervals = _extract_passiveValve_intervals(bpod)
446494

495+
task_version = _load_task_protocol(session_path)
447496
audio = ephys_fpga.get_sync_fronts(sync, sync_map["audio"], tmin=treplay[0])
448-
passiveTone_intervals, passiveNoise_intervals = _extract_passiveAudio_intervals(audio)
497+
passiveTone_intervals, passiveNoise_intervals = _extract_passiveAudio_intervals(audio,
498+
task_version)
449499

450500
passiveStims_df = np.concatenate(
451501
[passiveValve_intervals, passiveTone_intervals, passiveNoise_intervals], axis=1
@@ -493,8 +543,10 @@ def extract_replay_debug(
493543
passiveValve_intervals = _extract_passiveValve_intervals(bpod)
494544
plot_valve_times(passiveValve_intervals, ax=ax)
495545

546+
task_version = _load_task_protocol(session_path)
496547
audio = ephys_fpga.get_sync_fronts(sync, sync_map["audio"], tmin=treplay[0])
497-
passiveTone_intervals, passiveNoise_intervals = _extract_passiveAudio_intervals(audio)
548+
passiveTone_intervals, passiveNoise_intervals = _extract_passiveAudio_intervals(audio,
549+
task_version)
498550
plot_audio_times(passiveTone_intervals, passiveNoise_intervals, ax=ax)
499551

500552
passiveStims_df = np.concatenate(

ibllib/io/extractors/video_motion.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ def align_motion(self, period=(-np.inf, np.inf), side='left', sd_thresh=10, disp
154154
# TODO Add function arg to make grayscale
155155
self.alignment.frames = \
156156
vidio.get_video_frames_preload(camera_path, frame_numbers, mask=roi)
157+
assert self.alignment.frames.size != 0
157158
except AssertionError:
158159
self.log.error('Failed to open video')
159160
return None, None, None

ibllib/pipes/ephys_alignment.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ def __init__(self, xyz_picks, chn_depths=None, track_prev=None,
3535
self.xyz_samples = histology.interpolate_along_track(self.xyz_track,
3636
self.sampling_trk -
3737
self.sampling_trk[0])
38+
# ensure none of the track is outside the y or x lim of atlas
39+
xlim = np.bitwise_and(self.xyz_samples[:, 0] > self.brain_atlas.bc.xlim[0],
40+
self.xyz_samples[:, 0] < self.brain_atlas.bc.xlim[1])
41+
ylim = np.bitwise_and(self.xyz_samples[:, 1] < self.brain_atlas.bc.ylim[0],
42+
self.xyz_samples[:, 1] > self.brain_atlas.bc.ylim[1])
43+
rem = np.bitwise_and(xlim, ylim)
44+
self.xyz_samples = self.xyz_samples[rem]
3845

3946
self.region, self.region_label, self.region_colour, self.region_id\
4047
= self.get_histology_regions(self.xyz_samples, self.sampling_trk, self.brain_atlas)

0 commit comments

Comments
 (0)